1. Para que serve io.Copy
Se voce esta comecando agora, parece que entrada e saida sao algo «sobre arquivos» ou «sobre rede» (e hoje de proposito nao vamos entrar nem em um nem em outro). Mas, na verdade, entrada e saida sao sobre movimentacao de bytes: de uma string para um buffer, de um buffer para stdout, de um buffer para outro, de um Reader para um Writer. E se voce for escrever esse ciclo for { Read... Write... } toda vez, vai acabar suspeitando rapidamente que programadores sao pessoas que reescrevem voluntariamente a mesma coisa.
Em Go, essa suspeita foi organizada com cuidado na biblioteca padrao: io.Copy faz exatamente o que voce escreveria manualmente, so que de forma mais confiavel e mais curta. E uma daquelas funcoes que «primeiro parecem magia» e depois viram o seu botao diario de «faz direito». E sim — ela nao substitui o entendimento de Read/Write, ela apenas e construida em cima deles.
A propria ideia de «trabalhar com fluxos por meio de interfaces, e nao por tipos concretos» combina muito bem com o estilo geral de Go: contratos pequenos e tratamento explicito de erros. Nesse mesmo espirito, tambem funciona o uso padrao de error como valor.
2. io.Copy(dst, src): o que retorna e por que int64
io.Copy parece muito simples:
written, err := io.Copy(dst, src)
A primeira vista parece: «bom, apenas copia e pronto». Mas aqui cada palavra importa.
dst e um io.Writer, ou seja, «onde escrevemos».
src e um io.Reader, ou seja, «de onde lemos».
written e quantos bytes foram realmente gravados em dst. O tipo e int64, porque o fluxo pode ser grande, e int em sistemas de 32 bits (ou em geral) nao precisa ser suficiente para «muitos bytes».
Mais importante ainda e como interpretar o resultado:
- err == nil normalmente significa «copiamos ate o fim do fluxo».
- err != nil significa «algo deu errado durante o processo».
- ao mesmo tempo, written pode ser 0 ou maior que zero, e isso nao e bug, mas uma parte normal do contrato de fluxos: «progresso parcial».
Vamos ao exemplo mais «estéril» em memoria, sem arquivos e sem rede — string → buffer:
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
src := strings.NewReader("hello, stream")
var dst bytes.Buffer
n, err := io.Copy(&dst, src)
fmt.Printf("n=%d err=%v dst=%q\n", n, err, dst.String())
// n=13 err=<nil> dst="hello, stream"
}
Aqui fica facil ver que io.Copy retornou o tamanho e nao retornou erro.
Por baixo do pano, io.Copy faz o que discutimos antes: le em partes para um buffer, escreve em partes, repete ate o fim ou ate um erro. So que ele escolhe o buffer sozinho (e faz isso de maneira sensata), e para voce sobra cuidar do sentido do programa, e nao da «moldura em torno do Read».
3. Progresso parcial: written > 0 junto com err != nil
Um dos bugs mais desagradaveis de um iniciante em I/O e a ideia de que, se err != nil, entao nada funcionou. Para fluxos, isso esta errado. A realidade e esta: o fluxo pode ter conseguido transmitir parte dos dados e depois falhar. Ou o Writer pode ter aceitado parte dos dados e depois se recusar a continuar (por exemplo, «acabou o espaco» — de forma geral).
Isso se relaciona com o mesmo principio que vimos nos contratos basicos de Read/Write: a operacao pode retornar «parte do resultado + erro». Aqui, no exemplo de io.Copy, da para ver em geral como Copy e apenas um passo comum entre operacoes que podem terminar com erro; o importante e que o erro nao apaga o que foi feito antes dele.
Para sentir isso na pratica, vamos criar um Writer «malvado» que aceita apenas uma quantidade limitada de bytes e depois passa a falhar.
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
type failingWriter struct {
w io.Writer
limit int
wrote int
}
func (fw *failingWriter) Write(p []byte) (int, error) {
if fw.wrote >= fw.limit {
return 0, fmt.Errorf("no space left (limit=%d)", fw.limit)
}
can := fw.limit - fw.wrote
if len(p) > can {
p = p[:can]
}
n, err := fw.w.Write(p)
fw.wrote += n
return n, err
}
func main() {
src := strings.NewReader("abcdefghijklmnop")
var dst bytes.Buffer
fw := &failingWriter{w: &dst, limit: 5}
written, err := io.Copy(fw, src)
fmt.Printf("written=%d err=%v dst=%q\n", written, err, dst.String())
// written=5 err=no space left (limit=5) dst="abcde"
}
Isto e o que significa progresso parcial: io.Copy informa honestamente que conseguiu gravar 5 bytes, e informa honestamente que nao conseguiu continuar escrevendo.
Como lidar com isso na pratica? Com calma e sem gestos dramaticos:
- written e uma diagnostica util e, as vezes, uma informacao de negocio util (por exemplo, «conseguimos enviar N bytes»).
- err e a razao de por que «nao foi ate o fim».
- se sua operacao precisa ser «tudo ou nada», entao o progresso parcial deve ser visto como um problema no nivel do protocolo: ou faca rollback (se possivel), ou escreva por meio de protocolos seguros de atualizacao (mas isso ja e um tema grande a parte).
E sim, a ideia de «vamos simplesmente repetir io.Copy mais uma vez» normalmente nao corrige a situacao: os dados ja «foram parcialmente embora», e a repeticao costuma levar a duplicacao.
4. io.CopyN: «exatamente N bytes» e «no maximo N»
Com io.Copy tudo e bem intuitivo: «copie ate o fim». Mas, as vezes, voce precisa de outra coisa: «copie exatamente N bytes». Por exemplo, voce faz uma previa dos primeiros 50 bytes, ou le um cabecalho fixo, ou espera um tamanho estritamente definido pelo protocolo.
Para isso existe io.CopyN:
written, err := io.CopyN(dst, src, n)
Aqui o ponto principal esta na palavra N: a funcao tenta copiar exatamente n bytes. Se houver menos dados em src, a copia terminara antes, e voce recebera um erro (muitas vezes sera io.EOF, porque o fluxo «acabou antes de esperarmos»). Ao mesmo tempo, written ainda pode ser maior que zero — o que ja foi levado, permanece levado.
Exemplo minimo: vamos pegar uma origem de 3 bytes e tentar copiar 10:
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
src := strings.NewReader("hey")
var dst bytes.Buffer
written, err := io.CopyN(&dst, src, 10)
fmt.Printf("written=%d err=%v dst=%q\n", written, err, dst.String())
// written=3 err=EOF dst="hey"
}
Esse comportamento e muito logico se voce le CopyN como um «contrato de volume». Havia menos dados — o contrato nao foi cumprido, entao surge o erro.
Aqui e importante nao confundir duas tarefas parecidas:
- «Copiar no maximo N bytes» — isso e uma limitacao suave (por exemplo, «uma previa, mas nao falhe se o texto for menor»). Isso pode ser feito de forma conveniente com io.LimitReader + io.Copy.
- «Copiar exatamente N bytes» — isso e uma exigencia rigida. Para isso, use io.CopyN.
E essa diferenca ajuda muito a ler codigo alheio: se voce ve CopyN, o autor provavelmente queria exatamente «tantos bytes», e o erro por falta de dados e uma parte esperada da logica.
5. Tratamento de erros: contexto e preservacao da causa
Quando io.Copy retorna um erro, ele pode ser bem «de baixo nivel». Mesmo que seja apenas EOF, muitas vezes e util entender em qual operacao isso aconteceu: «copiando o relatorio de tarefas», «fazendo uma previa», «duplicando a saida».
Em Go, costuma-se retornar o erro para cima, adicionando contexto com fmt.Errorf("...: %w", err). Essa e uma pratica classica: erro e valor, e ele pode ser enriquecido com informacao.
Além disso, e importante nao transformar o erro em texto puro se voce quiser preservar a «causa» por dentro. E para isso que serve o %w (wrapping), para que, acima, seja possivel distinguir, por exemplo, io.EOF de outra coisa.
Vamos criar uma pequena utilidade: movemos dados de src para dst, e embrulhamos os erros de forma que fique claro o que exatamente foi copiado.
package main
import (
"fmt"
"io"
)
func copyWithContext(dst io.Writer, src io.Reader, op string) (int64, error) {
n, err := io.Copy(dst, src)
if err != nil {
return n, fmt.Errorf("%s: %w", op, err)
}
return n, nil
}
func main() {}
Observe um detalhe agradavel: retornamos n mesmo com erro. Isso e honesto: a funcao informa tanto «quanto conseguiu» quanto «por que nao foi ate o fim».
Essa ideia combina muito com a abordagem de «erros como valores» e com o fato de que, em Go, e conveniente escrever pequenos wrappers ao redor de operacoes padrao para eliminar codigo repetitivo e tornar o sentido mais visivel.
6. Exemplo: relatorio de tarefas como fluxo
Agora vamos criar um trecho que parece desenvolvimento real: temos dados (tarefas), queremos formar um relatorio em texto e envia-lo para «qualquer lugar». Antes, ja aprendemos a escrever em io.Writer via fmt.Fprintln e a acumular a saida em bytes.Buffer. Agora vamos adicionar a «transferencia» cuidadosa via io.Copy e a previa via io.CopyN.
Modelo e geracao do relatorio em buffer
Suponha que temos um modelo simples Task:
package main
import (
"fmt"
"io"
)
type Task struct {
ID int
Title string
Done bool
}
func writeTasksReport(w io.Writer, tasks []Task) {
for _, t := range tasks {
fmt.Fprintf(w, "#%d [%v] %s\n", t.ID, t.Done, t.Title)
}
}
func main() {}
A funcao writeTasksReport nao sabe nada sobre console, arquivos e outras romanticas coisas. Ela simplesmente escreve texto em um fluxo.
«Montar → transferir»: bytes.Buffer + io.Copy
Agora queremos: «montar o relatorio na memoria e depois transferi-lo para dst por um meio padrao». Para isso, bytes.Buffer e ideal: ele e io.Writer e tambem io.Reader.
package main
import (
"bytes"
"fmt"
"io"
)
func exportReport(dst io.Writer, tasks []Task) (int64, error) {
var buf bytes.Buffer
writeTasksReport(&buf, tasks)
n, err := io.Copy(dst, &buf)
if err != nil {
return n, fmt.Errorf("export report: %w", err)
}
return n, nil
}
func main() {}
O sentido aqui e duplo.
Primeiro, a «geracao do relatorio» esta separada de «onde exibimos».
Segundo, io.Copy nos da um comportamento padrao para transferir dados e o par padrao (written, err).
Previa do relatorio: io.CopyN como «os primeiros N bytes»
As vezes voce quer uma «saida curta»: os primeiros 40 bytes do relatorio, por exemplo, para mostrar ao usuario um trecho (ou para depuracao — sim, nos tambem somos humanos).
package main
import (
"bytes"
"fmt"
"io"
)
func exportReportPreview(dst io.Writer, tasks []Task, max int64) (int64, error) {
var buf bytes.Buffer
writeTasksReport(&buf, tasks)
n, err := io.CopyN(dst, &buf, max)
if err != nil {
return n, fmt.Errorf("export preview: %w", err)
}
return n, nil
}
func main() {}
Aqui existe uma sutileza: se o relatorio for menor que max, CopyN retornara erro (muitas vezes EOF). Isso e normal se voce realmente queria «exatamente N». Se, porem, voce queria «no maximo N», entao o melhor e usar a versao suave via io.LimitReader + io.Copy.
Mini demonstracao em main
Vamos juntar tudo em uma execucao minima:
package main
import (
"fmt"
"os"
)
func main() {
tasks := []Task{
{ID: 1, Title: "ler sobre io.Copy", Done: false},
{ID: 2, Title: "nao esquecer de tratar err", Done: false},
}
n, err := exportReport(os.Stdout, tasks)
fmt.Printf("written=%d err=%v\n", n, err)
// (acima sera impresso o relatorio)
// written=... err=<nil>
}
Sim, este e um exemplo «de console», mas a sacada e justamente que os.Stdout e apenas um io.Writer. No lugar dele poderia estar um buffer, um log, um fluxo de rede, qualquer coisa.
Pequeno diagrama: como pensar em io.Copy
As vezes e util ter uma imagem, para o cerebro nao tentar «inventar magia». io.Copy e apenas uma «ponte» entre interfaces:
flowchart LR
R["src: io.Reader"] -->|"Read(...)"| C["io.Copy"]
C -->|"Write(...)"| W["dst: io.Writer"]
C --> N["written (int64)"]
C --> E["err (error)"]
E exatamente por isso todo o tema se encaixa tao bem: voce aprende a combinar pequenos blocos — interfaces, buffers, wrappers e transferencia padrao.
7. Erros tipicos ao usar io.Copy / io.CopyN
Erro #1: achar que err != nil significa «nada foi gravado».
A entrada/saida em fluxo nao funciona assim: parte dos dados pode ter passado, e io.Copy vai informar isso honestamente por meio de written. Se o seu codigo joga fora written e trata tudo como «0 ou sucesso», voce perde informacao importante e dificulta a depuracao.
Erro #2: usar io.CopyN como «copie no maximo N, mas sem reclamar».
CopyN e um contrato rigido de volume. Se houver menos dados, ocorrera erro (normalmente EOF), e isso nao e um «incidente aleatorio», mas um comportamento esperado. Quando voce precisa de um «no maximo N» suave, e melhor limitar a origem com io.LimitReader e copiar com o io.Copy normal.
Erro #3: nao adicionar contexto ao erro e depois ficar adivinhando onde tudo quebrou.
Erros de I/O frequentemente vem de um nivel inferior e, por si so, nao explicam o que exatamente voce estava tentando fazer. Embrulhe-os com fmt.Errorf("...: %w", err), para que, acima, fique visivel o sentido da operacao. Esse estilo segue diretamente a ideia geral de que error e um valor que pode ser enriquecido com contexto.
Erro #4: perder a «causa» do erro ao transforma-lo em string.
Se, em vez de %w, voce usar %v, vai manter apenas o texto e perder a possibilidade de distinguir causas programaticamente. Para nos, agora basta criar o habito: se voce esta «subindo» um erro, normalmente vale a pena preservar o original com %w.
Erro #5: esperar que io.Copy «feche tudo sozinho».
io.Copy trata apenas da transferencia de bytes. Ele nao «possui» os recursos, entao nao pode decidir corretamente quando fecha-los. Quem fecha e quem abriu: esse e o principio geral de gerenciamento cuidadoso de recursos em Go, frequentemente expresso com defer.
GO TO FULL VERSION