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.Reader— slice 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:
- Fmt — formatação de I/O com Fprintf e Fscanf
- Log — logging com io.Writer
- Interface — design baseado em interfaces mínimas
- Channel — comunicação entre goroutines
- Error — tratamento de erros em operações de I/O
- API REST com Go — I/O em servidores HTTP
- Go para Backend — padrões de streaming
- Testes em Go — testando Readers e Writers
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.