O que é IO em Go?

O pacote io é um dos pilares fundamentais de Go, definindo as interfaces primitivas para operações de entrada e saída (Input/Output). As interfaces io.Reader e io.Writer são provavelmente as abstrações mais importantes de toda a linguagem — dezenas de pacotes da biblioteca padrão as implementam, e seu design minimalista permite composição poderosa de streams de dados.

A filosofia por trás do pacote io é simples: em vez de trabalhar com tipos concretos, pratique programar contra interfaces. Um arquivo, uma conexão de rede, um buffer em memória, um corpo de resposta HTTP — todos implementam io.Reader e/ou io.Writer. Código que trabalha com essas interfaces funciona automaticamente com qualquer fonte ou destino de dados.

As interfaces fundamentais

// io.Reader — qualquer coisa de onde você pode ler bytes
type Reader interface {
    Read(p []byte) (n int, err error)
}

// io.Writer — qualquer coisa para onde você pode escrever bytes
type Writer interface {
    Write(p []byte) (n int, err error)
}

Essas duas interfaces são surpreendentemente poderosas. Com apenas um method cada, elas possibilitam todo o ecossistema de I/O de Go.

io.Reader em Detalhe

A interface io.Reader tem um único método: Read(p []byte) (n int, err error). Ele lê até len(p) bytes e retorna quantos bytes foram lidos e um possível error. Quando não há mais dados, retorna io.EOF.

package main

import (
    "fmt"
    "io"
    "strings"
)

func main() {
    // strings.NewReader cria um Reader a partir de uma string
    reader := strings.NewReader("Olá, Go!")

    buf := make([]byte, 4) // lê 4 bytes por vez

    for {
        n, err := reader.Read(buf)
        if n > 0 {
            fmt.Printf("Lidos %d bytes: %s\n", n, buf[:n])
        }
        if err == io.EOF {
            fmt.Println("Fim dos dados")
            break
        }
        if err != nil {
            fmt.Println("Erro:", err)
            break
        }
    }
}

Saída:

Lidos 4 bytes: Olá,
Lidos 4 bytes:  Go!
Fim dos dados

Tipos que implementam io.Reader:

  • *os.File — arquivos
  • *strings.Reader — strings
  • *bytes.Readerslice de bytes
  • *bytes.Buffer — buffer de bytes
  • *http.Response.Body — respostas HTTP
  • *net.Conn — conexões de rede
  • *gzip.Reader — dados comprimidos

io.Writer em Detalhe

A interface io.Writer recebe bytes e os escreve em algum destino:

func exemploWriter() {
    // Escrever para arquivo
    arquivo, _ := os.Create("saida.txt")
    defer arquivo.Close()

    // arquivo implementa io.Writer
    arquivo.Write([]byte("Dados no arquivo\n"))

    // fmt.Fprintf aceita io.Writer
    fmt.Fprintf(arquivo, "Valor: %d\n", 42)

    // bytes.Buffer também é um Writer
    var buf bytes.Buffer
    buf.Write([]byte("Dados no buffer"))
    fmt.Println(buf.String())

    // os.Stdout é um Writer
    io.WriteString(os.Stdout, "Direto no terminal\n")
}

io.Copy — Transferência Eficiente

io.Copy é a ferramenta principal para transferir dados de um Reader para um Writer. Ele usa um buffer interno de 32KB e lida com toda a lógica de loop de leitura/escrita:

func copiarArquivo(origem, destino string) error {
    src, err := os.Open(origem)
    if err != nil {
        return fmt.Errorf("falha ao abrir origem: %w", err)
    }
    defer src.Close()

    dst, err := os.Create(destino)
    if err != nil {
        return fmt.Errorf("falha ao criar destino: %w", err)
    }
    defer dst.Close()

    bytesCopied, err := io.Copy(dst, src)
    if err != nil {
        return fmt.Errorf("falha ao copiar: %w", err)
    }

    fmt.Printf("Copiados %d bytes\n", bytesCopied)
    return nil
}

Download de arquivo da internet

func downloadArquivo(url, caminho string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    arquivo, err := os.Create(caminho)
    if err != nil {
        return err
    }
    defer arquivo.Close()

    _, err = io.Copy(arquivo, resp.Body)
    return err
}

io.CopyN e io.CopyBuffer

// Copia no máximo N bytes
io.CopyN(dst, src, 1024) // copia até 1KB

// Usa buffer customizado (útil para controlar alocação)
buf := make([]byte, 8*1024) // buffer de 8KB
io.CopyBuffer(dst, src, buf)

io.ReadAll — Ler Tudo de Uma Vez

io.ReadAll lê todos os bytes de um Reader até EOF. Conveniente para respostas HTTP e arquivos pequenos, mas cuidado com dados grandes — tudo vai para memória:

func lerResposta(url string) ([]byte, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    // Lê todo o corpo da resposta
    dados, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("falha ao ler resposta: %w", err)
    }

    return dados, nil
}

// Para arquivos grandes, prefira io.Copy ou processamento streaming

io.Pipe — Comunicação entre Goroutines

io.Pipe cria um par Reader/Writer sincronizado, ideal para conectar código que produz dados com código que consome dados em goroutines diferentes:

func exemploIoPipe() {
    pr, pw := io.Pipe()

    // Goroutine produtora
    go func() {
        defer pw.Close()

        encoder := json.NewEncoder(pw)
        for i := 0; i < 5; i++ {
            encoder.Encode(map[string]int{"valor": i})
            time.Sleep(100 * time.Millisecond)
        }
    }()

    // Goroutine consumidora (na goroutine principal)
    decoder := json.NewDecoder(pr)
    for {
        var dados map[string]int
        if err := decoder.Decode(&dados); err != nil {
            if err == io.EOF {
                break
            }
            log.Fatal(err)
        }
        fmt.Printf("Recebido: %v\n", dados)
    }
}

O Pipe é síncrono — Write bloqueia até que Read consuma os dados, e vice-versa. Isso fornece backpressure natural, similar a channels mas para streams de bytes.

Caso prático: upload streaming

func uploadJSON(url string, dados interface{}) (*http.Response, error) {
    pr, pw := io.Pipe()

    // Codifica JSON diretamente no pipe
    go func() {
        err := json.NewEncoder(pw).Encode(dados)
        pw.CloseWithError(err)
    }()

    // O pipe é o body da requisição — streaming sem buffer intermediário
    return http.Post(url, "application/json", pr)
}

io.MultiReader — Concatenar Readers

io.MultiReader combina múltiplos Readers em sequência, como se fossem um único Reader:

func exemploMultiReader() {
    cabecalho := strings.NewReader("=== RELATÓRIO ===\n")
    corpo := strings.NewReader("Dados do relatório...\n")
    rodape := strings.NewReader("=== FIM ===\n")

    // Combina os três em sequência
    completo := io.MultiReader(cabecalho, corpo, rodape)

    // Lê tudo como um stream único
    io.Copy(os.Stdout, completo)
}

// Caso prático: adicionar header/footer a upload
func uploadComHeader(header []byte, conteudo io.Reader) io.Reader {
    return io.MultiReader(
        bytes.NewReader(header),
        conteudo,
    )
}

io.MultiWriter — Escrever para Múltiplos Destinos

io.MultiWriter escreve os mesmos dados para múltiplos Writers simultaneamente:

func exemploMultiWriter() {
    arquivo, _ := os.Create("app.log")
    defer arquivo.Close()

    // Escreve para stdout E arquivo ao mesmo tempo
    multi := io.MultiWriter(os.Stdout, arquivo)

    fmt.Fprintln(multi, "Esta mensagem vai para ambos")

    // Útil com log
    log.SetOutput(multi)
    log.Println("Log duplicado")
}

io.TeeReader — Ler e Copiar

io.TeeReader cria um Reader que, conforme é lido, copia os dados para um Writer. Ideal para logar/inspecionar streams sem alterar o fluxo:

func exemploTeeReader() {
    // Resposta HTTP que queremos logar enquanto processa
    resp, _ := http.Get("https://api.exemplo.com/dados")
    defer resp.Body.Close()

    // Cria arquivo de log
    logFile, _ := os.Create("resposta.log")
    defer logFile.Close()

    // TeeReader: lê do body e copia para o arquivo simultaneamente
    tee := io.TeeReader(resp.Body, logFile)

    // Decodifica normalmente — os dados são gravados no log automaticamente
    var dados MeuTipo
    json.NewDecoder(tee).Decode(&dados)

    // logFile agora contém o JSON bruto da resposta
}

Calcular hash enquanto lê

func copiarComHash(dst io.Writer, src io.Reader) (int64, string, error) {
    h := sha256.New()

    // TeeReader copia para o hash enquanto io.Copy copia para o destino
    tee := io.TeeReader(src, h)

    n, err := io.Copy(dst, tee)
    if err != nil {
        return 0, "", err
    }

    hash := hex.EncodeToString(h.Sum(nil))
    return n, hash, nil
}

io.LimitReader — Limitar Leitura

io.LimitReader envolve um Reader e limita a quantidade de bytes que podem ser lidos. Essencial para proteger contra dados excessivos:

func lerCorpoHTTP(r *http.Request) ([]byte, error) {
    // Limita leitura a 1MB — protege contra requests maliciosos
    limitado := io.LimitReader(r.Body, 1<<20) // 1MB

    dados, err := io.ReadAll(limitado)
    if err != nil {
        return nil, err
    }

    return dados, nil
}

io.NopCloser — Adicionar Close a um Reader

io.NopCloser envolve um Reader e adiciona um método Close() que não faz nada. Útil para satisfazer a interface io.ReadCloser:

func criarResponse(corpo string) *http.Response {
    return &http.Response{
        StatusCode: 200,
        Body:       io.NopCloser(strings.NewReader(corpo)),
        Header:     make(http.Header),
    }
}

Composição de Interfaces IO

Uma das maiores forças do pacote io é a composição de interfaces simples em interfaces mais poderosas:

// Interfaces compostas do pacote io
type ReadWriter interface {
    Reader
    Writer
}

type ReadCloser interface {
    Reader
    Closer
}

type WriteCloser interface {
    Writer
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

type ReadSeeker interface {
    Reader
    Seeker
}

Essas interfaces compostas permitem que funções exijam exatamente as capacidades que precisam, seguindo o princípio de interfaces mínimas de Go: “aceite interfaces, retorne structs”.

// Aceita qualquer coisa que pode ler e fechar
func processarStream(rc io.ReadCloser) error {
    defer rc.Close()

    dados, err := io.ReadAll(rc)
    if err != nil {
        return err
    }

    return processar(dados)
}

// Funciona com arquivo, resposta HTTP, conexão de rede...
processarStream(arquivo)
processarStream(resp.Body)
processarStream(conn)

Implementando Reader e Writer Customizados

Criar seus próprios Readers e Writers é simples — basta implementar o method correspondente:

// Reader que gera zeros infinitamente
type ZeroReader struct{}

func (z ZeroReader) Read(p []byte) (int, error) {
    for i := range p {
        p[i] = 0
    }
    return len(p), nil
}

// Writer que conta bytes escritos
type ContadorWriter struct {
    Total int64
    Inner io.Writer
}

func (c *ContadorWriter) Write(p []byte) (int, error) {
    n, err := c.Inner.Write(p)
    c.Total += int64(n)
    return n, err
}

// Uso
func main() {
    contador := &ContadorWriter{Inner: os.Stdout}
    fmt.Fprintln(contador, "Mensagem de teste")
    fmt.Printf("Bytes escritos: %d\n", contador.Total)
}

Próximos Passos

Para aprofundar seus conhecimentos sobre I/O e streaming em Go:


Perguntas Frequentes

Qual a diferença entre io.ReadAll e io.Copy?

io.ReadAll lê todo o conteúdo de um Reader para a memória como um slice de bytes. io.Copy transfere dados de um Reader para um Writer em chunks, sem carregar tudo na memória. Para dados grandes (arquivos, downloads), use io.Copy para evitar uso excessivo de memória. Para dados pequenos (respostas de API, configs), io.ReadAll é mais conveniente.

Por que io.Reader e io.Writer são tão importantes em Go?

Essas duas interfaces são o pilar do design de I/O em Go porque permitem composição e reutilização. Código que aceita io.Reader funciona automaticamente com arquivos, strings, buffers, conexões de rede, dados comprimidos, e qualquer tipo customizado. Isso elimina duplicação e torna o código naturalmente extensível sem alterar funções existentes.

Como testo código que usa io.Reader/Writer?

Use strings.NewReader() para simular entrada e bytes.Buffer para capturar saída nos testing. Essas implementações em memória são perfeitas para testes porque não precisam de arquivos ou rede. Exemplo: var buf bytes.Buffer; minhaFuncao(&buf); assert(buf.String(), "esperado").

Quando usar io.Pipe vs channels?

Use io.Pipe quando precisar transferir streams de bytes entre goroutines — é ideal para conectar encoders com escritores, ou transformar dados em pipeline. Use channels para enviar valores tipados discretos. Pipe fornece backpressure automática e trabalha com qualquer código que aceita Reader/Writer. Channels são mais adequados para mensagens estruturadas e select multiplexing.