CodeGym /Cursos /Go SELF /io.Copy / io.CopyN

io.Copy / io.CopyN

Go SELF
Nível 26, Lição 3
Disponível

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:

  1. written e uma diagnostica util e, as vezes, uma informacao de negocio util (por exemplo, «conseguimos enviar N bytes»).
  2. err e a razao de por que «nao foi ate o fim».
  3. 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.

1
Tarefa
Go SELF, nível 26, lição 3
Bloqueado
Cópia para o buffer
Cópia para o buffer
1
Tarefa
Go SELF, nível 26, lição 3
Bloqueado
Pré-visualização por limite
Pré-visualização por limite
1
Tarefa
Go SELF, nível 26, lição 3
Bloqueado
Cópia de N bytes
Cópia de N bytes
1
Tarefa
Go SELF, nível 26, lição 3
Bloqueado
Writer que falha
Writer que falha
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION