Entrada e saída de dados
A entrada de dados acontece quando um programa recebe alguma informação de uma fonte externa, exemplo, quando digitamos um nome no terminal e o programa recebe esse texto, estamos realizando uma operação de entrada.
Já a saída de dados acontece quando o programa envia alguma informação para fora dele, como mostrar uma mensagem no terminal ou escrever em um arquivo. Esses dois processos são bem simples de se entender.
stdin, stdout e stderr em Go
O Linux e outros sistemas operacionais usam três fluxos padrão de entrada e saída, conhecidos como stdin, stdout e stderr. Eles servem para permitir que os programas recebam dados, enviem resultados e apresentem mensagens de erro, normalmente por meio do terminal.
O stdin (Standard Input) é responsável pela entrada de dados, como informações digitadas pelo usuário. O stdout (Standard Output) é responsável pela saída dos resultados do programa, já o stderr (Standard Error) é usado para enviar mensagens de erro, separando elas dos resultados normais.
O Go e outras linguagens de programação usam esses mesmos fluxos para se comunicar com o ambiente onde o programa está sendo executado. Em Go, podemos acessar eles por meio do package os, usando os.Stdin, os.Stdout e os.Stderr.
package main
import (
"fmt"
"os"
)
func main() {
var nome string
// Recebe um valor do stdin.
fmt.Fscan(os.Stdin, &nome)
// Envia uma mensagem para o stdout.
fmt.Fprintln(os.Stdout, "Olá,", nome)
// Envia uma mensagem para o stderr.
fmt.Fprintln(os.Stderr, "Mensagem de erro")
}
As mensagens enviadas para stdout e stderr normalmente aparecem no mesmo terminal, mas usam fluxos diferentes. Isso permite salvar os resultados em um arquivo sem misturar ele com as mensagens de erro.
Escrevendo mensagens na saída de erro
Até agora, usamos o fmt.Println() para mostrar mensagens na saída padrão (stdout). Mas quando precisamos mostrar uma mensagem de erro, podemos usar fmt.Fprintln() junto com os.Stderr.
O os.Stderr representa a saída padrão de erro do programa.
package main
import (
"fmt"
"os"
)
func main() {
// Escreve uma mensagem na saída padrão.
fmt.Println("Programa iniciado")
// Escreve uma mensagem na saída de erro.
fmt.Fprintln(os.Stderr, "Erro ao processar os dados")
}
As duas mensagens normalmente vão aparecer no terminal, mas são enviadas por fluxos separados. Isso permite, por exemplo, salvar a saída normal do programa em um arquivo sem misturá-la com as mensagens de erro, isso é mais usado para administradores de sistema Linux e Unix.
Lendo dados do terminal - stdin
Vamos ver como como receber informações digitadas pelo usuário, permitindo que o programa trabalhe com dados que não foram definidos diretamente no código.
Em Go, podemos receber esses dados por meio da entrada padrão (stdin), usando funções do package fmt ou recursos do package bufio, dependendo de como queremos fazer a leitura.
A função fmt.Scan permite receber valores da entrada padrão (stdin) e armazenar em variáveis. Quando o programa encontra essa função, ele aguarda a entrada de dados para continuar a execução.
package main
import "fmt"
func main() {
var nome string
fmt.Print("Digite seu nome: ")
// Aguarda o usuário digitar um nome.
fmt.Scan(&nome)
fmt.Println("Olá,", nome)
}
O operador & permite passar o endereço de memória da variável, para que a função consiga modificar seu valor diretamente. Isso é necessário porque o fmt.Scan precisa armazenar o dado recebido na variável informada.
Embora seja comum pressionar Enter para enviar o texto pelo terminal, o fmt.Scan separa a entrada por espaços e quebras de linha.
Recebendo diferentes tipos de dados
O fmt.Scan também consegue receber valores de outros tipos, como int e float64. O Go tenta interpretar o texto recebido de acordo com o tipo da variável.
package main
import "fmt"
func main() {
var nome string
var idade int
var altura float64
fmt.Print("Digite seu nome: ")
fmt.Scan(&nome)
fmt.Print("Digite sua idade: ")
fmt.Scan(&idade)
fmt.Print("Digite sua altura: ")
fmt.Scan(&altura)
fmt.Printf("Nome: %s\n", nome)
fmt.Printf("Idade: %d\n", idade)
fmt.Printf("Altura: %.2f\n", altura)
}
Se informarmos um texto onde o programa espera um número inteiro, por exemplo, a leitura não será concluída corretamente. Por isso, precisamos tratar possíveis erros de entrada, algo que veremos mais adiante nesta sessão.
Recebendo vários valores com fmt.Scan
Também podemos receber vários valores em uma única chamada, informando mais de uma variável.
package main
import "fmt"
func main() {
var nome string
var idade int
fmt.Print("Digite seu nome e idade, separando-os por espaço ou quebra de linha: ")
// Recebe dois valores separados por espaço.
fmt.Scan(&nome, &idade)
fmt.Printf("Nome: %s, Idade: %d\n", nome, idade)
}
O fmt.Scan identifica os valores separados por espaços ou quebras de linha e armazena cada um na variável correspondente. Nesse caso, o primeiro valor será armazenado em nome e o segundo em idade.
O cuidado com espaços em textos
O fmt.Scan funciona bem quando queremos receber palavras ou números separados, mas pode não ser a melhor escolha para ler textos completos, já que ele considera espaços como separadores entre os valores.
Para receber uma linha completa, incluindo os espaços, podemos usar o package bufio.
Lendo uma linha completa com bufio
O package bufio oferece recursos para trabalhar com leitura e escrita usando um buffer, que é uma área temporária de memória usada para armazenar dados durante uma operação de entrada ou saída.
Isso permite maior controle, sem precisar processar cada parte imediatamente. Para ler uma linha completa do terminal, podemos criar um leitor com bufio.NewReader.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// Cria um leitor para a entrada padrão.
leitor := bufio.NewReader(os.Stdin)
fmt.Print("Digite seu nome completo: ")
// Lê o texto até encontrar uma quebra de linha.
nome, err := leitor.ReadString('\n')
if err != nil {
fmt.Println("Erro ao ler o nome:", err)
return
}
fmt.Print("Nome:", nome)
}
O os.Stdin representa a entrada padrão, enquanto bufio.NewReader cria um leitor que permite receber os dados desse fluxo.
O método ReadString lê os dados até encontrar o caractere informado. Nesse caso, '\n' representa uma quebra de linha, normalmente gerada quando pressionamos Enter.
Diferentemente do fmt.Scan, o ReadString mantém os espaços entre as palavras.
Um detalhe importante é que a quebra de linha também faz parte do texto retornado. Isso pode causar comportamentos inesperados quando precisamos comparar ou armazenar o conteúdo recebido.
Removendo a quebra de linha com strings.TrimSpace
Quando usamos ReadString('\n'), o texto recebido normalmente inclui a quebra de linha. Por exemplo, se digitarmos:
Ana Maria
O valor armazenado será parecido com:
"Ana Maria\n"
Para remover os espaços e as quebras de linha do início e do final do texto, podemos usar strings.TrimSpace:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
leitor := bufio.NewReader(os.Stdin)
fmt.Print("Digite seu nome completo: ")
// Recebe a linha completa.
nome, err := leitor.ReadString('\n')
if err != nil {
fmt.Println("Erro ao ler o nome:", err)
return
}
// Remove espaços e quebras de linha das extremidades.
nome = strings.TrimSpace(nome)
fmt.Println("Nome:", nome)
}
O strings.TrimSpace é útil quando queremos trabalhar apenas com o conteúdo digitado, sem os caracteres extras das extremidades, exemplo:
texto := " Ana Maria \n"
texto = strings.TrimSpace(texto)
// Resultado: "Ana Maria"
Essa função não remove os espaços entre as palavras, apenas aqueles que aparecem no início e no final do texto.
Lendo dados com bufio.Scanner
Outra forma de receber informações do terminal é usando bufio.Scanner. O Scanner permite ler dados de um fluxo, normalmente separando o conteúdo em linhas.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// Cria um scanner para a entrada padrão.
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Digite seu nome completo: ")
// Aguarda a leitura de uma linha.
if scanner.Scan() {
nome := scanner.Text()
fmt.Println("Nome:", nome)
}
// Verifica se ocorreu algum erro na leitura.
if err := scanner.Err(); err != nil {
fmt.Println("Erro ao ler o nome:", err)
}
}
O método Scan() tenta ler o próximo valor do fluxo, por padrão, esse valor corresponde a uma linha de texto. Quando a leitura é concluída com sucesso, ele retorna true. Depois, usamos:
nome := scanner.Text()
O método Text() retorna o texto lido, sem incluir a quebra de linha. Isso significa que, diferentemente de ReadString('\n'), normalmente não precisamos remover o \n manualmente.
O Scanner também possui um limite padrão para o tamanho dos dados que consegue processar em uma leitura, por isso não é a melhor escolha para linhas muito grandes.
Tratando erros de entrada
Quando recebemos dados do terminal, não podemos assumir que a entrada será sempre válida, exemplo, podemos esperar um número inteiro, mas o usuário pode digitar uma palavra. Para esses casos, podemos usar o fmt.Scan, ele retorna dois valores, sendo a quantidade de itens lidos corretamente e um possível erro.
E ai podemos verificar esse erro antes de continuar:
package main
import "fmt"
func main() {
var idade int
fmt.Print("Digite sua idade: ")
// Recebe a quantidade de valores lidos e um possível erro.
_, err := fmt.Scan(&idade)
if err != nil {
fmt.Println("Erro: digite um número válido.")
return
}
fmt.Println("Idade informada:", idade)
}
Nesse exemplo, usamos _ para ignorar a quantidade de valores lidos, porque queremos verificar apenas se ocorreu algum erro.
Esse tratamento impede que o programa continue usando um valor que não foi recebido corretamente. Também precisamos lembrar que uma entrada pode ser válida para o tipo, mas inválida para a regra do programa.
Por exemplo, -10 é um número inteiro válido, mas não representa uma idade adequada para um cadastro. Nesse caso, precisamos fazer uma validação adicional.
Argumentos de linha de comando
Os argumentos de linha de comando são informações que podemos passar para um programa no momento em que ele é executado, permitindo modificar seu comportamento sem precisar alterar o código. Por exemplo, no Linux, quando executamos ls -l /tmp, estamos passando dois argumentos para o programa ls, sendo -l uma opção e /tmp o diretório que queremos consultar.
Em Go, podemos receber esses argumentos por meio de os.Args, que pertence ao package os e representa um slice de strings.
Recebendo argumentos com os.Args
Vamos criar um programa que recebe um nome e uma idade diretamente pelo terminal.
package main
import (
"fmt"
"os"
)
func main() {
// Verifica se os argumentos foram informados.
if len(os.Args) < 3 {
fmt.Println("Informe seu nome e idade")
return
}
// Recebe os argumentos.
nome := os.Args[1]
idade := os.Args[2]
fmt.Println("Nome:", nome)
fmt.Println("Idade:", idade)
}
Podemos executar o programa passando os argumentos depois do comando:
go run . Carlos 25
Resultado:
Nome: Carlos
Idade: 25
Oos.Args contém os argumentos recebidos pelo programa, onde cada posição do slice representa uma informação. O primeiro elemento (os.Args[0]), representa o nome ou caminho usado para iniciar o programa. Por isso, começamos a receber nossos argumentos a partir da posição 1. Também verificamos len(os.Args) antes de acessar os argumentos, evitando um panic caso não sejam informados todos os valores necessários.
Convertendo e validando argumentos
Os argumentos recebidos por os.Args são sempre strings, mesmo quando informamos números. Por exemplo, se passarmos 25, o Go receberá "25", e não um valor do tipo int. Para converter esse valor, podemos usar strconv.Atoi(), que transforma uma string em um número inteiro.
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Informe sua idade")
return
}
// Converte o argumento para int.
idade, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Println("Idade inválida")
return
}
// Verifica se a idade é válida.
if idade < 0 {
fmt.Println("A idade não pode ser negativa")
return
}
fmt.Println("Idade:", idade)
}
Se informarmos um texto no lugar do número, o strconv.Atoi() retornará um erro, permitindo que o programa identifique a entrada inválida. Também podemos passar textos com espaços como um único argumento, colocando o conteúdo entre aspas.
go run . "Carlos Silva"
Nesse caso, "Carlos Silva" será recebido como um único argumento.
Entrada segura de dados
Quando recebemos informações pelo terminal usando fmt.Scan() ou bufio.Scanner, os caracteres digitados normalmente aparecem na tela. Isso funciona para informações comuns, como nomes e números, mas não é adequado para dados sensíveis, principalmente senhas. Para esses casos, precisamos receber a informação sem mostrar os caracteres digitados, evitando que outras pessoas consigam visualizar a senha enquanto ela é informada.
Lendo senhas sem mostrar no terminal
No Linux e em outros sistemas operacionais, o terminal normalmente possui um recurso chamado echo, responsável por mostrar os caracteres conforme eles são digitados. Quando digitamos uma senha, podemos desativar temporariamente esse comportamento, permitindo que o programa receba os dados sem exibi-los na tela.
Em Go, podemos fazer isso usando o package golang.org/x/term, que oferece a função term.ReadPassword(). Essa biblioteca não faz parte da biblioteca padrão do Go, então precisamos adicioná-la ao projeto.
go get golang.org/x/term
Usando golang.org/x/term
A função term.ReadPassword() permite receber uma senha diretamente do terminal, sem mostrar os caracteres digitados.
package main
import (
"fmt"
"os"
"golang.org/x/term"
)
func main() {
fmt.Print("Digite sua senha: ")
// Recebe a senha sem mostrar os caracteres no terminal.
senha, err := term.ReadPassword(int(os.Stdin.Fd()))
// Adiciona uma quebra de linha após a digitação.
fmt.Println()
if err != nil {
fmt.Println("Erro ao receber senha:", err)
return
}
// Aqui podemos trabalhar com a senha recebida.
_ = senha
fmt.Println("Senha recebida")
}
Resultado:
Digite sua senha:
Senha recebida
Nesse exemplo, o usuário digita a senha e pressiona Enter, mas os caracteres não aparecem no terminal. Observe esta parte:
term.ReadPassword(int(os.Stdin.Fd()))
O os.Stdin.Fd() retorna o descritor de arquivo da entrada padrão, que é um identificador usado pelo sistema operacional para representar um arquivo ou fluxo aberto. A função ReadPassword() recebe esse identificador para acessar o terminal e controlar a exibição dos caracteres durante a leitura.
Também precisamos observar que ReadPassword() retorna a senha como []byte, e não como string. Essa função precisa de um terminal compatível, então pode retornar um erro se a entrada padrão estiver redirecionada de um arquivo ou de outro programa.
Cuidados com informações sensíveis
Mesmo recebendo a senha sem mostrar os caracteres no terminal, ainda precisamos tomar alguns cuidados. Não devemos receber senhas diretamente pelos argumentos de linha de comando, como:
./programa --senha minhaSenha123
Isso acontece porque os argumentos podem ficar registrados no histórico do terminal ou aparecer nas informações dos processos do sistema operacional. Também devemos evitar mostrar senhas com fmt.Println(), registrar seus valores em logs ou armazená-las em arquivos de texto sem proteção. Outro detalhe é que term.ReadPassword() apenas impede que os caracteres sejam mostrados durante a digitação. Isso não significa que a senha esteja criptografada ou protegida automaticamente depois de ser recebida.
A senha continuará disponível na memória do programa enquanto estiver sendo processada. Por isso, devemos evitar manter cópias desnecessárias desses dados e não convertê-los para string sem necessidade. Quando precisamos armazenar senhas de usuários, não devemos salvar seus valores originais. Nesse caso, normalmente usamos algoritmos próprios para armazenamento de senhas, como Argon2id ou bcrypt, que serão mais relevantes quando estudarmos autenticação em aplicações.
Interfaces de entrada e saída
A linguagem Go oeferece as interfaces io.Reader e io.Writer para trabalharmos com leitura e escrita de dados sem depender diretamente de uma fonte ou destino específico. Por exemplo, podemos ler informações de um arquivo, de uma string ou de uma conexão de rede. Embora essas fontes sejam diferentes, todas podem seguir a mesma interface de leitura.
Isso permite criar funções que trabalham com diferentes fontes de dados, sem precisar desenvolver uma função específica para cada uma.
io.Reader
O io.Reader é uma interface do package io que define como os dados devem ser lidos. Qualquer tipo que implemente o método Read() com a assinatura esperada pode ser tratado como um io.Reader. A interface possui a seguinte definição:
type Reader interface {
Read(p []byte) (n int, err error)
}
O método Read() recebe um slice de bytes ([]byte), onde os dados serão armazenados, e retorna a quantidade de bytes lidos e um possível erro. Por exemplo, podemos criar um Reader para ler o conteúdo de uma string:
package main
import (
"fmt"
"io"
"strings"
)
func main() {
// Cria um Reader a partir de uma string.
leitor := strings.NewReader("Ana Maria")
// Cria um espaço para receber até 20 bytes.
dados := make([]byte, 20)
// Lê os dados e retorna a quantidade de bytes recebidos.
n, err := leitor.Read(dados)
if err != nil {
fmt.Println("Erro ao ler:", err)
return
}
fmt.Println(string(dados[:n]))
}
Resultado:
Ana Maria
O strings.NewReader() cria um leitor que permite acessar o conteúdo de uma string por meio da interface io.Reader. Depois, usamos Read() para preencher o slice dados com os bytes recebidos. A variável n informa quantos bytes foram lidos, permitindo acessar apenas a parte preenchida do slice. O método pode retornar menos bytes do que o espaço disponível. Para ler todo o conteúdo de uma fonte até o final, podemos usar io.ReadAll(), que faz as leituras necessárias e retorna os dados como []byte.
io.Writer
O io.Writer funciona de maneira parecida com io.Reader, mas é responsável pela escrita de dados. Ele define o método Write(), que recebe uma sequência de bytes e escreve esses dados em algum destino.
type Writer interface {
Write(p []byte) (n int, err error)
}
O método retorna a quantidade de bytes escritos e um possível erro. Por exemplo, podemos escrever informações diretamente no terminal usando os.Stdout, que implementa a interface io.Writer.
package main
import (
"fmt"
"io"
"os"
)
func main() {
// Define a saída padrão como destino da escrita.
var destino io.Writer = os.Stdout
// Escreve uma mensagem no terminal.
_, err := destino.Write([]byte("Olá, Go!\n"))
if err != nil {
fmt.Println("Erro ao escrever:", err)
return
}
}
Resultado:
Olá, Go!
Quando declaramos destino como io.Writer e atribuímos os.Stdout, ele passa a possuir o método Write() exigido pela interface. Isso significa que podemos trocar o destino por outro que também implemente io.Writer, como um arquivo aberto para escrita, sem precisar mudar a forma como escrevemos os dados.
Trabalhando com Reader e Writer
Uma das vantagens dessas interfaces é permitir que diferentes tipos trabalhem juntos. Podemos, por exemplo, ler informações de uma fonte usando io.Reader e enviar esses dados para um destino usando io.Writer. O package io possui a função io.Copy(), que faz essa transferência automaticamente.
package main
import (
"fmt"
"io"
"os"
"strings"
)
func main() {
// Cria um Reader a partir de uma string.
leitor := strings.NewReader("Ana Maria\nCarlos Silva\n")
// Cria um arquivo que receberá os dados.
arquivo, err := os.Create("usuarios.txt")
if err != nil {
fmt.Println("Erro ao criar arquivo:", err)
return
}
defer arquivo.Close()
// Copia os dados do Reader para o Writer.
_, err = io.Copy(arquivo, leitor)
if err != nil {
fmt.Println("Erro ao copiar dados:", err)
return
}
fmt.Println("Dados copiados com sucesso!")
}
Depois da execução, o arquivo usuarios.txt terá este conteúdo:
Ana Maria
Carlos Silva
Observe esta parte:
io.Copy(arquivo, leitor)
A função io.Copy() recebe primeiro o destino, que precisa implementar io.Writer, e depois a origem, que precisa implementar io.Reader. Nesse exemplo, leitor representa a origem dos dados, enquanto arquivo representa o destino onde eles serão escritos. O io.Copy() faz a leitura e a escrita dos dados sem precisar carregar todo o conteúdo na memória de uma só vez. Isso é útil principalmente quando trabalhamos com arquivos grandes ou conexões de rede.
Podemos representar esse funcionamento assim:
io.Reader io.Writer
| |
v v
strings.Reader -----> os.File (arquivo)
io.Copy()
O interessante é que não precisamos mudar a lógica de cópia quando trocamos a fonte ou o destino, desde que os tipos implementem as interfaces necessárias. Por exemplo, poderíamos substituir strings.NewReader() por um arquivo aberto com os.Open(), porque ambos implementam io.Reader. Da mesma forma, poderíamos substituir o arquivo de destino por os.Stdout, porque ambos implementam io.Writer. Essas interfaces são bastante comuns em Go, principalmente quando trabalhamos com arquivos, requisições HTTP e conexões de rede, porque permitem que diferentes recursos compartilhem os mesmos métodos de leitura e escrita.