Este artigo se concentra nas práticas recomendadas para escrever código Go. É composto no estilo de apresentação, mas sem os slides usuais. Vamos tentar brevemente e claramente percorrer cada item.
Primeiro, você precisa concordar sobre o significado das
melhores práticas para uma linguagem de programação. Aqui você pode se lembrar das palavras de Russ Cox, diretor técnico da Go:
Engenharia de software é o que acontece com a programação, se você adicionar o fator tempo e outros programadores.
Assim, Russ distingue entre os conceitos de
programação e
engenharia de software . No primeiro caso, você escreve um programa para si mesmo; no segundo, cria um produto no qual outros programadores trabalharão com o tempo. Engenheiros vêm e vão. As equipes crescem ou encolhem. Novos recursos são adicionados e os bugs são corrigidos. Essa é a natureza do desenvolvimento de software.
Conteúdo
1. Princípios fundamentais
Talvez eu seja um dos primeiros usuários do Go entre vocês, mas essa não é minha opinião pessoal. Estes princípios básicos estão subjacentes ao próprio Go:
- Simplicidade
- Legibilidade
- Produtividade
Nota Observe que eu não mencionei "desempenho" ou "simultaneidade". Existem idiomas mais rápidos que o Go, mas certamente não podem ser comparados com simplicidade. Existem linguagens que colocam o paralelismo como a principal prioridade, mas elas não podem ser comparadas em termos de legibilidade ou produtividade de programação.
Desempenho e simultaneidade são atributos importantes, mas não tão importantes quanto simplicidade, legibilidade e produtividade.Simplicidade
“Simplicidade é um pré-requisito para a confiabilidade” - Edsger Dijkstra
Por que lutar pela simplicidade? Por que é importante que os programas Go sejam simples?
Cada um de nós se deparou com um código incompreensível, certo? Quando você tem medo de fazer uma alteração, porque isso interrompe outra parte do programa que você não entende e não sabe como consertar. Essa é a dificuldade.
“Existem duas maneiras de projetar software: a primeira é torná-lo tão simples que não há falhas óbvias, e a segunda é torná-lo tão complexo que não há falhas óbvias. O primeiro é muito mais difícil. ” - C.E. R. Hoar
Complexidade transforma software confiável em não confiável. Complexidade é o que mata projetos de software. Portanto, a simplicidade é o objetivo final da Go. Quaisquer que sejam os programas que escrevemos, eles devem ser simples.
1.2 Legibilidade
“A legibilidade é parte integrante da manutenção” - Mark Reinhold, JVM Conference, 2018
Por que é importante que o código seja legível? Por que devemos nos esforçar para facilitar a leitura?
“Os programas devem ser escritos para as pessoas e as máquinas apenas os executam” - Hal Abelson e Gerald Sassman, “Estrutura e interpretação de programas de computador”
Não apenas os programas Go, mas geralmente todo o software é escrito por pessoas para pessoas. O fato de as máquinas também processarem código é secundário.
Uma vez que o código escrito será lido repetidamente pelas pessoas: centenas, se não milhares de vezes.
“A habilidade mais importante para um programador é a capacidade de comunicar idéias de maneira eficaz.” - Gaston Horker
A legibilidade é a chave para entender o que um programa faz. Se você não consegue entender o código, como mantê-lo? Se o software não puder ser suportado, ele será reescrito; e essa pode ser a última vez que sua empresa usa o Go.
Se você está escrevendo um programa para si mesmo, faça o que funciona para você. Mas se isso faz parte de um projeto conjunto ou o programa for usado por tempo suficiente para alterar os requisitos, funções ou o ambiente em que ele trabalha, seu objetivo é tornar o programa sustentável.
O primeiro passo para escrever o software suportado é garantir que o código esteja claro.
1.3 Produtividade
“O design é a arte de organizar o código para que ele funcione hoje, mas sempre suporte mudanças.” - Sandy Mets
Como último princípio básico, quero nomear a produtividade do desenvolvedor. Esse é um tópico importante, mas tudo se resume à proporção: quanto tempo você gasta em trabalho útil e quanto - aguardando uma resposta de ferramentas ou andanças sem esperança em uma base de código incompreensível. Os programadores de Go devem sentir que podem lidar com muito trabalho.
É uma piada que a linguagem Go tenha sido desenvolvida enquanto o programa C ++ estava compilando. A compilação rápida é um recurso essencial do Go e um fator essencial para atrair novos desenvolvedores. Embora os compiladores estejam sendo aprimorados, em geral, a compilação de minutos em outros idiomas leva alguns segundos no Go. Os desenvolvedores do So Go se sentem tão produtivos quanto os programadores em linguagens dinâmicas, mas sem problemas com a confiabilidade dessas linguagens.
Se falamos fundamentalmente sobre a produtividade dos desenvolvedores, os programadores do Go entendem que a leitura do código é essencialmente mais importante do que a sua criação. Nessa lógica, o Go chega ao ponto de usar as ferramentas para formatar todo o código em um determinado estilo. Isso elimina a menor dificuldade em aprender o dialeto específico de um projeto específico e ajuda a identificar erros, porque eles simplesmente
parecem errados em comparação com o código regular.
Os programadores da Go não passam dias depurando erros estranhos de compilação, scripts de compilação complexos ou implementando código em um ambiente de produção. E o mais importante, eles não perdem tempo tentando entender o que um colega escreveu.
Quando os desenvolvedores do Go falam sobre
escalabilidade , eles significam produtividade.
2. Identificadores
O primeiro tópico que discutiremos -
identificadores , é sinônimo de
nomes : nomes de variáveis, funções, métodos, tipos, pacotes e assim por diante.
“O mau nome é um sintoma de mau design” - Dave Cheney
Dada a sintaxe limitada do Go, os nomes de objetos têm um enorme impacto na legibilidade do programa. A legibilidade é um fator chave no bom código, portanto, a escolha de bons nomes é crucial.
2.1 Identificadores de nome com base na clareza e não na brevidade
“É importante que o código seja óbvio. O que você pode fazer em uma linha, você deve fazer em três. ” - Ukia Smith
O Go não é otimizado para one-liners complicados ou o número mínimo de linhas em um programa. Não otimizamos o tamanho do código fonte no disco, nem o tempo necessário para digitar o programa no editor.
“Um bom nome é como uma boa piada. Se você precisar explicar, não será mais engraçado. ” - Dave Cheney
A chave da máxima clareza são os nomes que escolhemos para identificar programas. Que qualidades são inerentes a um bom nome?
- Um bom nome é conciso . Não precisa ser o mais curto, mas não contém excesso. Possui uma alta relação sinal / ruído.
- Um bom nome é descritivo . Descreve o uso de uma variável ou constante, não o conteúdo. Um bom nome descreve o resultado de uma função ou o comportamento de um método, não uma implementação. O objetivo do pacote, não seu conteúdo. Quanto mais precisamente o nome descreve o que identifica, melhor.
- Um bom nome é previsível . Por um nome, você deve entender como o objeto será usado. Os nomes devem ser descritivos, mas também é importante seguir a tradição. É isso que os programadores do Go querem dizer quando dizem "idiomático" .
Vamos considerar em mais detalhes cada uma dessas propriedades.
2.2 Comprimento do ID
Às vezes, o estilo de Go é criticado por nomes curtos de variáveis. Como
disse Rob Pike, "os programadores da Go querem identificadores do tamanho
correto ".
Andrew Gerrand oferece identificadores mais longos para indicar importância.
“Quanto maior a distância entre a declaração de um nome e o uso de um objeto, maior deve ser o nome” - Andrew Gerrand
Assim, algumas recomendações podem ser feitas:
- Nomes curtos de variáveis são bons se a distância entre a declaração e o último uso for pequena.
- Nomes de variáveis longos devem se justificar; quanto mais longos, mais importantes devem ser. Os títulos detalhados contêm pouco sinal em relação ao peso na página.
- Não inclua o nome do tipo no nome da variável.
- Nomes constantes devem descrever o valor interno, não como o valor é usado.
- Prefira variáveis de letra única para loops e ramificações, palavras separadas para parâmetros e valores de retorno, várias palavras para funções e declarações no nível do pacote.
- Prefira palavras simples para métodos, interfaces e pacotes.
- Lembre-se de que o nome do pacote faz parte do nome que o chamador usa para referência.
Considere um exemplo.
type Person struct { Name string Age int }
Na décima linha, uma variável do intervalo
p
declarada e é chamada apenas uma vez da próxima linha. Ou seja, a variável permanece na página por um período muito curto. Se o leitor estiver interessado no papel de
p
no programa, ele só precisará ler duas linhas.
Para comparação,
people
declaradas em parâmetros de função e sete linhas ao vivo. O mesmo vale para
sum
e
count
, para que justifiquem seus nomes mais longos. O leitor precisa digitalizar mais códigos para encontrá-los: isso justifica os nomes mais distintos.
Você pode escolher
s
para
sum
c
(ou
n
) para
count
, mas isso reduz a importância de todas as variáveis no programa para o mesmo nível. Você pode substituir as
people
por
p
, mas haverá um problema, o que chamar de variável de iteração
for ... range
. Uma única
person
parecerá estranha, porque uma variável de iteração de curta duração recebe um nome mais longo do que vários valores dos quais é derivada.
Dica . Separe o fluxo de funções com linhas vazias, pois as linhas vazias entre parágrafos interrompem o fluxo de texto. Na AverageAge
, temos três operações consecutivas. Primeiro, verificando a divisão por zero, depois a conclusão da idade total e número de pessoas e a última - o cálculo da idade média.
2.2.1 O principal é o contexto
É importante entender que a maioria das dicas de nomenclatura é específica ao contexto. Eu gosto de dizer que isso é um princípio, não uma regra.
Qual é a diferença entre
i
e
index
? Por exemplo, você não pode dizer inequivocamente que esse código
for index := 0; index < len(s); index++ {
fundamentalmente mais legível do que
for i := 0; i < len(s); i++ {
Acredito que a segunda opção não é pior, porque nesse caso a região
i
ou o
index
limitado pelo corpo do loop
for
, e a verbosidade adicional acrescenta pouco ao entendimento do programa.
Mas qual dessas funções é mais legível?
func (s *SNMP) Fetch(oid []int, index int) (int, error)
ou
func (s *SNMP) Fetch(o []int, i int) (int, error)
Neste exemplo,
oid
é uma abreviação de SNMP Object ID e a abreviação adicional para
o
força ao ler código para alternar de uma notação documentada para uma notação mais curta no código. Da mesma forma, reduzir o
index
para
i
torna mais difícil entender, porque nas mensagens SNMP, o sub-valor de cada OID é chamado de índice.
Dica . Não combine parâmetros formais longos e curtos em um anúncio.
2.3 Não nomeie variáveis por tipo
Você não chama seus animais de estimação de "cachorro" e "gato", certo? Pelo mesmo motivo, você não deve incluir o nome do tipo no nome da variável. Ele deve descrever o conteúdo, não seu tipo. Considere um exemplo:
var usersMap map[string]*User
Que bom é esse anúncio? Vemos que este é um mapa e tem algo a ver com o
*User
Tipo de
*User
: isso provavelmente é bom. Mas
usersMap
é
realmente um mapa, e Go, como uma linguagem de tipo estaticamente, não permitirá o uso acidental de um nome como esse quando uma variável escalar for necessária, portanto o sufixo do
Map
é redundante.
Considere uma situação em que outras variáveis são adicionadas:
var ( companiesMap map[string]*Company productsMap map[string]*Products )
Agora, temos três variáveis do tipo mapa:
usersMap
,
companiesMap
e
productsMap
, e todas as linhas são mapeadas para tipos diferentes. Sabemos que esses são mapas e também sabemos que o compilador lançará um erro se tentarmos usar
companiesMap
onde o código espera
map[string]*User
. Nessa situação, fica claro que o sufixo
Map
não melhora a clareza do código, são apenas caracteres extras.
Sugiro evitar sufixos semelhantes ao tipo de uma variável.
Dica . Se o nome de users
não descrever a essência com clareza suficiente, o usersMap
também.
Esta dica também se aplica aos parâmetros de função. Por exemplo:
type Config struct {
O nome da
config
para o parâmetro
*Config
é redundante. Já sabemos que este é
*Config
; ele é imediatamente escrito ao lado.
Nesse caso, considere
conf
ou
c
se a vida útil da variável for curta o suficiente.
Se em algum momento da nossa área houver mais de um
*Config
, os nomes
conf1
e
conf2
menos significativos que o
original
e
updated
, pois os últimos são mais difíceis de serem misturados.
Nota Não permita que nomes de pacotes roubem bons nomes de variáveis.
O nome do identificador importado contém o nome do pacote. Por exemplo, o tipo de context
pacote de context
será chamado context.Context
. Isso impossibilita o uso de uma variável ou tipo de context
no seu pacote.
func WriteLog(context context.Context, message string)
Isso não será compilado. É por isso que, ao declarar o context.Context
Tipos de context.Context
localmente, por exemplo, nomes como ctx
são tradicionalmente usados.
func WriteLog(ctx context.Context, message string)
2.4 Use um único estilo de nomeação
Outra propriedade de um bom nome é que deve ser previsível. O leitor deve entendê-lo imediatamente. Se esse é um nome
comum , o leitor tem o direito de assumir que não mudou o significado da época anterior.
Por exemplo, se o código contornar o descritor do banco de dados, sempre que o parâmetro for exibido, ele deverá ter o mesmo nome. Em vez de todos os tipos de combinações como
d *sql.DB
,
dbase *sql.DB
,
DB *sql.DB
e
database *sql.DB
, é melhor usar uma coisa:
db *sql.DB
É mais fácil entender o código. Se você
*sql.DB
db
, saberá que é
*sql.DB
e é declarado localmente ou fornecido pelo chamador.
Conselho semelhante em relação aos destinatários de um método; use o mesmo nome de destinatário para cada método desse tipo. Isso tornará mais fácil para o leitor entender o uso do receptor entre os vários métodos desse tipo.
Nota O Contrato de Nome Curto do Destinatário da Go contradiz as recomendações expressas anteriormente. Esse é um daqueles casos em que a escolha feita em um estágio inicial se torna o estilo padrão, como usar o CamelCase
vez de snake_case
.
Dica . O estilo Ir aponta para nomes de uma letra ou abreviações para destinatários derivados de seu tipo. Pode acontecer que o nome do destinatário às vezes esteja em conflito com o nome do parâmetro no método. Nesse caso, é recomendável aumentar um pouco o nome do parâmetro e não se esqueça de usá-lo sequencialmente.
Finalmente, algumas variáveis de uma letra são tradicionalmente associadas a loops e contagem. Por exemplo,
i
,
j
e
k
são geralmente variáveis indutivas em loops,
n
geralmente associado a um contador ou somador acumulador,
v
é uma abreviação típica de valor em uma função de codificação,
k
geralmente usado para uma chave de mapa e
s
frequentemente usado como uma abreviação de parâmetros do tipo
string
.
Como no exemplo
db
acima, os programadores
esperam que i
seja uma variável indutiva. Se o virem no código, esperam ver um loop em breve.
Dica . Se você tiver tantos loops aninhados que esgotou as variáveis i
, j
k
, poderá dividir a função em unidades menores.
2.5 Use um único estilo de declaração
Go tem pelo menos seis maneiras diferentes de declarar uma variável.
var x int = 1
var x = 1
var x int; x = 1
var x = int(1)
x := 1
Tenho certeza de que ainda não lembrei de tudo. Os desenvolvedores do Go provavelmente consideram isso um erro, mas é tarde demais para mudar qualquer coisa. Com esta escolha, como garantir um estilo uniforme?
Quero propor um estilo de declarar variáveis que eu mesmo tento usar sempre que possível.
Como o Go não possui conversões automáticas de um tipo para outro, no primeiro e terceiro exemplos, o tipo no lado esquerdo do operador de atribuição deve ser idêntico ao tipo no lado direito. O compilador pode inferir o tipo da variável declarada do tipo à direita, para que o exemplo possa ser escrito de forma mais concisa:
var players = 0 var things []Thing = nil var thing = new(Thing) json.Unmarshall(reader, thing)
Aqui, os
players
inicializados explicitamente em
0
, o que é redundante, porque o valor inicial dos
players
é zero em qualquer caso. Portanto, é melhor deixar claro que queremos usar um valor nulo:
var players int
E o segundo operador? Não podemos determinar o tipo e escrever
var things = nil
Porque
nil
não nil
tipo . Em vez disso, temos uma escolha: ou usamos um valor zero para cortar ...
var things []Thing
... ou criar uma fatia com zero elementos?
var things = make([]Thing, 0)
No segundo caso, o valor da fatia
não é zero e deixamos claro para o leitor usando uma forma abreviada de declaração:
things := make([]Thing, 0)
Isso diz ao leitor que decidimos inicializar explicitamente as
things
.
Então chegamos à terceira declaração:
var thing = new(Thing)
Aqui, a inicialização explícita da variável e a introdução da palavra-chave “única”
new
, que alguns programadores do Go não gostam. O uso da sintaxe curta recomendada gera
thing := new(Thing)
Isso deixa claro que
thing
explicitamente inicializado no resultado de
new(Thing)
, mas ainda deixa um
new
atípico. O problema pode ser resolvido usando um literal:
thing := &Thing{}
O que é semelhante ao
new(Thing)
, e essa duplicação perturba alguns programadores do Go. No entanto, isso significa que inicializamos explicitamente a
thing
com um ponteiro para
Thing{}
e um valor
Thing
de zero.
Mas é melhor levar em consideração o fato de que
thing
declarado com um valor zero e usar o endereço do operador para passar o endereço de
thing
em
json.Unmarshall
:
var thing Thing json.Unmarshall(reader, &thing)
Nota Obviamente, há exceções a qualquer regra. Por exemplo, às vezes duas variáveis estão intimamente relacionadas, por isso será estranho escrever
var min int max := 1000
Declaração mais legível:
min, max := 0, 1000
Para resumir:
- Ao declarar uma variável sem inicialização, use a sintaxe
var
.
- Ao declarar e inicializar explicitamente uma variável, use
:=
.
Dica . Explique explicitamente coisas complexas.
var length uint32 = 0x80
Aqui, o length
pode ser usado com a biblioteca, que requer um tipo numérico específico, e esta opção indica mais claramente que o comprimento do tipo é especificamente selecionado como uint32 do que na declaração curta:
length := uint32(0x80)
No primeiro exemplo, eu intencionalmente quebrei minha regra usando a declaração var com inicialização explícita. Um afastamento do padrão faz com que o leitor entenda que algo incomum está acontecendo.
2.6 Trabalho para a equipe
Eu já disse que a essência do desenvolvimento de software é a criação de código legível e suportado. A maior parte da sua carreira provavelmente funcionará em projetos conjuntos. Meu conselho nessa situação: siga o estilo adotado na equipe.
Alterar estilos no meio do arquivo é irritante. A consistência é importante, embora em detrimento da preferência pessoal. Minha regra geral é: se o código se encaixa no
gofmt
, o problema geralmente não vale a pena ser discutido.
Dica . Se você deseja renomear em toda a base de código, não misture isso com outras alterações. Se alguém usa git bisect, ele não gosta de percorrer milhares de renomeações para encontrar outro código modificado.
3. Comentários
Antes de passarmos a pontos mais importantes, quero dedicar alguns minutos para comentar.
“Um bom código tem muitos comentários e um código ruim precisa de muitos comentários.” - Dave Thomas e Andrew Hunt, programador pragmático
Os comentários são muito importantes para a legibilidade do programa. Cada comentário deve fazer uma - e apenas uma - de três coisas:
- Explique o que o código faz.
- Explique como ele faz isso.
- Explique o porquê .
O primeiro formulário é ideal para comentar caracteres públicos:
O segundo é ideal para comentários dentro de um método:
A terceira forma ("por quê") é única, pois não substitui nem substitui as duas primeiras. Tais comentários explicam os fatores externos que levaram à escrita do código em sua forma atual. Muitas vezes, sem esse contexto, é difícil entender por que o código é escrito dessa maneira.
return &v2.Cluster_CommonLbConfig{
Neste exemplo, pode não ficar claro imediatamente o que acontece quando HealthyPanicThreshold é definido como zero por cento. O comentário pretende esclarecer que um valor 0 desativa o limite de pânico.
3.1 Comentários em variáveis e constantes devem descrever seu conteúdo, não o objetivo
Eu disse anteriormente que o nome de uma variável ou constante deve descrever seu propósito. Mas um comentário sobre uma variável ou constante deve descrever exatamente o
conteúdo , não o
objetivo .
const randomNumber = 6
Neste exemplo, um comentário descreve
por que randomNumber
como 6 e de onde veio. O comentário não descreve onde o
randomNumber
será usado. Aqui estão mais alguns exemplos:
const ( StatusContinue = 100
No contexto do HTTP, o número
100
conhecido como
StatusContinue
, conforme definido na RFC 7231, seção 6.2.1.
Dica . Para variáveis sem valor inicial, o comentário deve descrever quem é responsável por inicializar essa variável.
Aqui, um comentário informa ao leitor que a função dowidth
responsável por manter o estado de sizeCalculationDisabled
.
Dica . Esconder à vista. Este é o conselho de Kate Gregory . Às vezes, o melhor nome para uma variável está oculto nos comentários.
Um comentário foi adicionado pelo autor porque o registry
nomes não explica suficientemente sua finalidade - este é um registro, mas qual é o registro?
Se você renomear uma variável como sqlDrivers, fica claro que ela contém drivers SQL.
var sqlDrivers = make(map[string]*sql.Driver)
Agora o comentário tornou-se redundante e pode ser excluído.
3.2 Sempre documente caracteres publicamente disponíveis
A documentação do seu pacote é gerada pelo godoc, portanto, você deve adicionar um comentário a cada caractere público declarado no pacote: uma variável, constante, função e método.
Aqui estão duas diretrizes do Guia de estilos do Google:
- Qualquer função pública que não seja óbvia nem concisa deve ser comentada.
- Qualquer função na biblioteca deve ser comentada, independentemente do tamanho ou complexidade.
package ioutil
Há uma exceção a esta regra: você não precisa documentar métodos que implementam a interface. Especificamente, não faça isso:
Este comentário não significa nada. Ele não diz o que o método faz: pior, ele envia para algum lugar para procurar documentação. Nesta situação, proponho excluir completamente o comentário.
Aqui está um exemplo do pacote
io
.
Observe que a declaração
LimitedReader
é imediatamente precedida pela função que a usa e a declaração
LimitedReader.Read
segue a declaração do
LimitedReader
. Embora o
LimitedReader.Read
si não esteja documentado, é possível entender que essa é uma implementação do
io.Reader
.
Dica . Antes de escrever uma função, escreva um comentário descrevendo-a. Se você acha difícil escrever um comentário, isso é um sinal de que o código que você está prestes a escrever será difícil de entender.
3.2.1 Não comente sobre código incorreto, reescreva-o
“Não comente código incorreto - reescreva-o” - Brian Kernighan
Não basta indicar nos comentários a dificuldade do fragmento de código. Se você se deparar com um desses comentários, inicie um ticket com um lembrete de refatoração. Você pode viver com dívida técnica, desde que seu valor seja conhecido.
Na biblioteca padrão, é habitual deixar comentários no estilo TODO com o nome do usuário que notou o problema.
Isso não é uma obrigação para corrigir o problema, mas o usuário indicado pode ser a melhor pessoa para fazer uma pergunta. Outros projetos acompanham o TODO com uma data ou número do ticket.
3.2.2 Em vez de comentar o código, refatore-o
“Bom código é a melhor documentação. Quando você estiver prestes a adicionar um comentário, faça a si mesmo a pergunta: “Como melhorar o código para que este comentário não seja necessário?” Refatore e deixe um comentário para torná-lo ainda mais claro. ” - Steve McConnell
As funções devem executar apenas uma tarefa. Se você deseja escrever um comentário porque algum fragmento não está relacionado ao restante da função, considere extraí-lo em uma função separada.
Recursos menores não são apenas mais claros, mas mais fáceis de testar separadamente. Quando você isolou o código em uma função separada, seu nome pode substituir um comentário.
4. Estrutura do pacote
“Escreva um código modesto: módulos que não mostram nada supérfluo para outros módulos e que não dependem da implementação de outros módulos” - Dave Thomas
Cada pacote é essencialmente um pequeno programa Go separado. Assim como a implementação de uma função ou método não importa para quem chama, a implementação das funções, métodos e tipos que compõem a API pública do seu pacote não importa.
Um bom pacote Go busca uma conectividade mínima com outros pacotes no nível do código-fonte, para que, à medida que o projeto cresça, as alterações em um pacote não sejam distribuídas em cascata por toda a base de código. Tais situações inibem muito os programadores que trabalham nessa base de código.
Nesta seção, falaremos sobre o design de pacotes, incluindo seu nome e dicas para escrever métodos e funções.
4.1 Um bom pacote começa com um bom nome
Um bom pacote Go começa com um nome de qualidade. Pense nisso como uma breve apresentação limitada a apenas uma palavra.Como os nomes das variáveis na seção anterior, o nome do pacote é muito importante. Não há necessidade de pensar nos tipos de dados deste pacote; é melhor fazer a pergunta: "Que serviço esse pacote fornece?" Normalmente, a resposta não é "Este pacote fornece o tipo X", mas "Este pacote permite que você se conecte via HTTP".Dica . Escolha um nome de pacote por sua funcionalidade, não por seu conteúdo.
4.1.1 Os bons nomes de pacotes devem ser exclusivos
Cada pacote tem um nome exclusivo no projeto. Não há dificuldade se você seguiu o conselho de dar nomes para os fins dos pacotes. Se os dois pacotes tiverem o mesmo nome, provavelmente:- .
- . , .
4.2 base
, common
util
Um motivo comum para nomes incorretos são os chamados pacotes de serviços , onde, com o tempo, vários ajudantes e códigos de serviço se acumulam. Como é difícil encontrar um nome único lá. Isso geralmente leva ao fato de que o nome do pacote é derivado do que ele contém : utilitários.Nomes como utils
ou helpers
são geralmente encontrados em grandes projetos, nos quais uma hierarquia profunda de pacotes está enraizada e as funções auxiliares são compartilhadas. Se você extrair alguma função em um novo pacote, a importação será interrompida. Nesse caso, o nome do pacote não reflete o objetivo do pacote, mas apenas o fato de a função de importação falhar devido à organização incorreta do projeto.Em tais situações, recomendo analisar de onde os pacotes são chamados.utils
helpers
e, se possível, mova as funções correspondentes para o pacote de chamada. Mesmo que isso implique duplicação de algum código auxiliar, é melhor do que introduzir uma dependência de importação entre dois pacotes.“[Um pouco] duplicação é muito mais barata que uma abstração errada” - Sandy Mets
Se funções utilitárias são usadas em muitos lugares, em vez de um pacote monolítico com funções utilitárias, é melhor criar vários pacotes, cada um dos quais se concentra em um aspecto.Dica . Use o plural para pacotes de serviços. Por exemplo, strings
para utilitários de processamento de cadeia.
Pacotes com nomes como base
ou common
são frequentemente encontrados quando uma certa funcionalidade comum de duas ou mais implementações ou tipos comuns para um cliente e um servidor é mesclada em um pacote separado. Acredito que, nesses casos, é necessário reduzir o número de pacotes combinando cliente, servidor e código comum em um pacote com um nome que corresponda à sua função.Por exemplo, para net/http
não fazer os pacotes individuais client
e server
, em vez disso, existem arquivos client.go
e server.go
com os tipos de dados correspondentes, bem como transport.go
para o transporte total.Dica . É importante lembrar que o nome do identificador inclui o nome do pacote.
- Uma função
Get
de um pacote net/http
se torna um http.Get
link de outro pacote.
- Um tipo
Reader
de um pacote é strings
transformado quando importado para outros pacotes strings.Reader
.
- A interface
Error
do pacote está net
claramente associada a erros de rede.
4.3 Volte rapidamente sem mergulhar fundo
Como o Go não usa exceções no fluxo de controle, não há necessidade de se aprofundar no código para fornecer uma estrutura try
e blocos de nível superior catch
. Em vez de uma hierarquia multinível, o código Go desce a tela à medida que a função progride. Meu amigo Matt Ryer chama essa prática de "linha de visão" .Isso é conseguido usando operadores de limite : blocos condicionais com uma pré-condição na entrada da função. Aqui está um exemplo do pacote bytes
: func (b *Buffer) UnreadRune() error { if b.lastRead <= opInvalid { return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune") } if b.off >= int(b.lastRead) { b.off -= int(b.lastRead) } b.lastRead = opInvalid return nil }
Ao entrar na função UnreadRune
, o estado é verificado b.lastRead
e, se a operação anterior não foi ReadRune
, um erro é retornado imediatamente. O restante da função funciona com base no que é b.lastRead
maior que opInvalid
.Compare com a mesma função, mas sem o operador de limite: func (b *Buffer) UnreadRune() error { if b.lastRead > opInvalid { if b.off >= int(b.lastRead) { b.off -= int(b.lastRead) } b.lastRead = opInvalid return nil } return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune") }
O corpo de uma ramificação bem-sucedida mais provável é incorporado na primeira condição if
e a condição para uma saída bem-sucedida return nil
deve ser descoberta combinando cuidadosamente os colchetes de fechamento . A última linha da função agora retorna um erro e você precisa acompanhar a execução da função no colchete de abertura correspondente para descobrir como chegar a esse ponto.Essa opção é mais difícil de ler, o que diminui a qualidade da programação e do suporte ao código; portanto, o Go prefere usar operadores de limite e retornar erros logo no início.4.4 Tornar o valor nulo útil
Cada declaração de variável, assumindo a ausência de um inicializador explícito, será automaticamente inicializada com um valor correspondente ao conteúdo da memória zerada, ou seja, zero . O tipo de valor é determinado por uma das opções: para tipos numéricos - zero, para tipos de ponteiros - zero, o mesmo para fatias, mapas e canais.A capacidade de sempre definir um valor padrão conhecido é importante para a segurança e a correção do seu programa e pode tornar seus programas Go mais fáceis e compactos. É isso que os programadores do Go têm em mente quando dizem: "Dê às estruturas um valor zero útil".Considere um tipo sync.Mutex
que contém dois campos inteiros que representam o estado interno do mutex. Esses campos são automaticamente nulos em qualquer declaração.sync.Mutex
. Esse fato é levado em consideração no código, portanto, o tipo é adequado para uso sem inicialização explícita. type MyInt struct { mu sync.Mutex val int } func main() { var i MyInt
Outro exemplo de um tipo com um valor nulo útil é bytes.Buffer
. Você pode declarar e começar a escrever nele sem inicialização explícita. func main() { var b bytes.Buffer b.WriteString("Hello, world!\n") io.Copy(os.Stdout, &b) }
O valor zero dessa estrutura significa que len
ambos cap
são iguais 0
, y array
, o ponteiro para a memória com o conteúdo da matriz de fatia de backup, valor nil
. Isso significa que você não precisa cortar explicitamente, você pode simplesmente declarar. func main() {
Nota . var s []string
semelhante às duas linhas comentadas no topo, mas não idênticas a elas. Há uma diferença entre um valor de fatia nulo e um valor de fatia de comprimento zero. O código a seguir será impresso como falso.
func main() { var s1 = []string{} var s2 []string fmt.Println(reflect.DeepEqual(s1, s2)) }
Uma propriedade útil, embora inesperada, de variáveis de ponteiro não inicializadas - ponteiros nulos - é a capacidade de chamar métodos em tipos que são nulos. Isso pode ser usado para fornecer facilmente valores padrão. type Config struct { path string } func (c *Config) Path() string { if c == nil { return "/usr/home" } return c.path } func main() { var c1 *Config var c2 = &Config{ path: "/export", } fmt.Println(c1.Path(), c2.Path()) }
4.5 Evitar estado do nível do pacote
A chave para escrever programas de fácil suporte que estão fracamente conectados é que a alteração de um pacote deve ter uma baixa probabilidade de afetar outro pacote que não depende diretamente do primeiro.Existem duas maneiras excelentes de obter conectividade fraca no Go:- Use interfaces para descrever o comportamento exigido por funções ou métodos.
- Evite status global.
No Go, podemos declarar variáveis no escopo de uma função ou método, bem como no escopo de um pacote. Quando uma variável está disponível publicamente, com um identificador com letra maiúscula, seu escopo é realmente global para todo o programa: qualquer pacote a qualquer momento vê o tipo e o conteúdo dessa variável.O estado global mutável fornece uma estreita relação entre as partes independentes do programa, pois as variáveis globais se tornam um parâmetro invisível para cada função no programa! Qualquer função que depende de uma variável global pode ser violada quando o tipo dessa variável é alterado. Qualquer função que depende do estado de uma variável global pode ser violada se outra parte do programa alterar essa variável.Como reduzir a conectividade que uma variável global cria:- Mova as variáveis correspondentes como campos para as estruturas que precisam delas.
- Use interfaces para reduzir a conexão entre o comportamento e a implementação desse comportamento.
5. Estrutura do projeto
Vamos falar sobre como os pacotes são combinados em um projeto. Geralmente é um único repositório Git.Como o pacote, cada projeto deve ter um objetivo claro. Se for uma biblioteca, deve fazer uma coisa, por exemplo, análise XML ou registro no diário. Você não deve combinar vários objetivos em um projeto, isso ajudará a evitar uma biblioteca assustadora common
.Dica . Na minha experiência, o repositório common
está intimamente associado ao maior consumidor, e isso dificulta a correção de versões anteriores (correções de porta traseira) sem atualizar common
o consumidor e o consumidor no estágio de bloqueio, o que leva a muitas mudanças não relacionadas, além de serem interrompidas pelo caminho API
Se você possui um aplicativo (aplicativo Web, controlador Kubernetes, etc.), o projeto pode ter um ou mais pacotes principais. Por exemplo, no meu controlador Kubernetes, há um pacote cmd/contour
que serve como um servidor implantado em um cluster Kubernetes e como um cliente de depuração.5.1 Menos pacotes, mas maiores
Na revisão de código, notei um dos erros típicos dos programadores que mudaram para o Go de outros idiomas: eles tendem a abusar de pacotes.GO não fornecer o elaborado sistema de visibilidade: a linguagem não é suficiente modificadores de acesso, como no Java ( public
, protected
, private
e implícita default
). Não há análogo de classes amigáveis do C ++.No Go, temos apenas dois modificadores de acesso: são identificadores públicos e privados, indicados pela primeira letra do identificador (maiúscula / minúscula). Se o identificador for público, seu nome começará com uma letra maiúscula e poderá ser referenciado por qualquer outro pacote Go.Nota . Você pode ouvir as palavras "exportado" ou "não exportado" como sinônimos para público e privado.
Dado os recursos limitados de controle de acesso, quais métodos podem ser usados para evitar hierarquias de pacotes excessivamente complexas?Dica . Em cada pacote, além cmd/
e internal/
deve estar presente o código fonte.
Eu já disse várias vezes que é melhor preferir menos pacotes maiores. Sua posição padrão deve ser não criar um novo pacote. Isso faz com que muitos tipos se tornem públicos, criando um escopo amplo e pequeno da API disponível. Abaixo, consideramos esta tese em mais detalhes.Dica . Veio de Java?
Se você é do mundo Java ou C #, lembre-se da regra tácita: um pacote Java é equivalente a um único arquivo de origem .go
. O pacote Go é equivalente a todo o módulo Maven ou assembly .NET.
5.1.1 Classificando o código por arquivo usando as instruções de importação
Se você organizar pacotes por serviço, faça o mesmo com os arquivos no pacote? Como saber quando dividir um arquivo .go
em vários? Como você sabe se você foi longe demais e precisa pensar em mesclar arquivos?Aqui estão as recomendações que eu uso:- Inicie cada pacote com um arquivo
.go
. Atribua a esse arquivo o mesmo nome que o diretório. Por exemplo, o pacote http
deve estar no arquivo http.go
no diretório http
.
- Conforme o pacote cresce, você pode dividir as várias funções em vários arquivos. Por exemplo, o arquivo
messages.go
conterá tipos Request
e Response
, client.go
tipo de Client
arquivo, server.go
servidor de tipo de arquivo .
- , . , .
- . ,
messages.go
HTTP- , http.go
, client.go
server.go
— HTTP .
. .
. Go . ( — Go). .
5.1.2 Prefira testes internos a externos
A ferramenta go
suporta o pacote testing
em dois lugares. Se você possui um pacote http2
, pode escrever um arquivo http2_test.go
e usar a declaração do pacote http2
. Ele compila o código http2_test.go
, como é parte do pacote http2
. No discurso coloquial, esse teste é chamado interno.A ferramenta go
também suporta uma declaração de pacote especial que termina com teste , ou seja http_test
. Isso permite que os arquivos de teste residam no mesmo pacote com o código, mas quando esses testes são compilados, eles não fazem parte do código do seu pacote, mas vivem em seu próprio pacote. Isso permite que você escreva testes como se outro pacote estivesse invocando seu código. Tais testes são chamados externos.Eu recomendo usar testes internos para testes de unidade. Isso permite que você teste cada função ou método diretamente, evitando a burocracia de testes externos.Mas é necessário colocar exemplos de funções de teste ( Example
) em um arquivo de teste externo . Isso garante que, quando visualizados no godoc, os exemplos recebam o prefixo do pacote apropriado e possam ser facilmente copiados.. , .
, , Go go
. , net/http
net
.
.go
, , .
5.1.3. , API
Se o seu projeto tiver vários pacotes, você poderá encontrar funções exportadas que devem ser usadas por outros pacotes, mas não para a API pública. Em tal situação, a ferramenta go
reconhece um nome de pasta especial internal/
que pode ser usado para colocar o código aberto para o seu projeto, mas fechado para outros.Para criar esse pacote, coloque-o em um diretório com um nome internal/
ou em seu subdiretório. Quando a equipe go
vê a importação do pacote com o caminho internal
, verifica o local do pacote de chamada em um diretório ou subdiretório internal/
.Por exemplo, um pacote .../a/b/c/internal/d/e/f
pode importar apenas um pacote de uma árvore de diretórios .../a/b/c
, mas não de todo .../a/b/g
ou qualquer outro repositório (consultedocumentação ).5.2 O menor pacote principal
Uma função main
e um pacote main
devem ter funcionalidade mínima, porque main.main
age como um singleton: um programa pode ter apenas uma função main
, incluindo testes.Como main.main
é um singleton, há muitas restrições nos objetos chamados: eles são chamados apenas durante main.main
ou main.init
e apenas uma vez . Isso dificulta a escrita de testes de código main.main
. Portanto, você precisa se esforçar para derivar o máximo de lógica possível da função principal e, idealmente, do pacote principal.Dica . func main()
deve analisar sinalizadores, abrir conexões com bancos de dados, registradores etc. e depois transferir a execução para um objeto de alto nível.
6. estrutura da API
O último conselho de design do projeto que considero o mais importante.Todas as frases anteriores são, em princípio, não vinculativas. Estas são apenas recomendações baseadas na experiência pessoal. Eu não forço muito essas recomendações em uma revisão de código.A API é outra questão: aqui, os erros são levados mais a sério, porque todo o resto pode ser corrigido sem quebrar a compatibilidade com versões anteriores: na maior parte, esses são apenas detalhes de implementação.Quando se trata de APIs públicas, vale a pena considerar seriamente a estrutura desde o início, porque as alterações subsequentes serão destrutivas para os usuários.6.1 APIs de design difíceis de abusar por design
“As APIs devem ser simples para uso adequado e difíceis para incorretas” - Josh Bloch
O conselho de Josh Bloch é talvez o mais valioso neste artigo. Se for difícil usar a API para coisas simples, todas as chamadas à API serão mais complicadas do que o necessário. Quando uma chamada de API é complexa e não óbvia, é provável que seja ignorada.6.1.1 Cuidado com as funções que aceitam vários parâmetros do mesmo tipo.
Um bom exemplo de uma API simples à primeira vista, mas difícil de usar, é quando ela requer dois ou mais parâmetros do mesmo tipo. Compare duas assinaturas de função: func Max(a, b int) int func CopyFile(to, from string) error
Qual é a diferença entre essas duas funções? Obviamente, um retorna no máximo dois números e o outro copia o arquivo. Mas este não é o ponto. Max(8, 10)
Max é comutativo : a ordem dos parâmetros não importa. Um máximo de oito e dez é dez, independentemente de oito e dez ou dez e oito serem comparados.Mas no caso do CopyFile, não é assim. CopyFile("/tmp/backup", "presentation.md") CopyFile("presentation.md", "/tmp/backup")
Quais desses operadores farão backup da sua apresentação e quais serão substituídos pela versão da semana passada? Você não pode saber até verificar a documentação. No curso da revisão de código, não está claro se a ordem dos argumentos está correta ou não. Mais uma vez, veja a documentação.Uma solução possível é a introdução de um tipo auxiliar responsável pela chamada correta CopyFile
. type Source string func (src Source) CopyTo(dest string) error { return CopyFile(dest, string(src)) } func main() { var from Source = "presentation.md" from.CopyTo("/tmp/backup") }
É CopyFile
sempre chamado corretamente aqui - isso pode ser declarado usando um teste de unidade - e pode ser feito em particular, o que reduz ainda mais a probabilidade de uso incorreto.Dica . Uma API com vários parâmetros do mesmo tipo é difícil de usar corretamente.
6.2 Projetar uma API para um caso de uso básico
Alguns anos atrás, fiz uma apresentação sobre o uso de opções funcionais para facilitar a API por padrão.A essência da apresentação foi que você deveria desenvolver uma API para o caso de uso principal. Em outras palavras, a API não deve exigir que o usuário forneça parâmetros extras que não lhe interessam.6.2.1 O uso de nil como parâmetro não é recomendado
Comecei dizendo que você não deve forçar o usuário a fornecer parâmetros de API que não lhe interessam. Isso significa projetar as APIs para o caso de uso principal (opção padrão).Aqui está um exemplo do pacote net / http. package http
ListenAndServe
aceita dois parâmetros: um endereço TCP para escutar nas conexões recebidas e http.Handler
para processar uma solicitação HTTP recebida. Serve
permite que o segundo parâmetro seja nil
. Nos comentários, note-se que geralmente o objeto de chamada realmente passa nil
, indicando um desejo de usá-lo http.DefaultServeMux
como um parâmetro implícito.Agora o chamador Serve
tem duas maneiras de fazer o mesmo. http.ListenAndServe("0.0.0.0:8080", nil) http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)
Ambas as opções fazem a mesma coisa.Este aplicativo se nil
espalha como um vírus. O pacote também http
possui um auxiliar http.Serve
, para que você possa imaginar a estrutura da função ListenAndServe
: func ListenAndServe(addr string, handler Handler) error { l, err := net.Listen("tcp", addr) if err != nil { return err } defer l.Close() return Serve(l, handler) }
Como ListenAndServe
ele permite que o chamador passe nil
para o segundo parâmetro, http.Serve
também suporta esse comportamento. De fato, está na http.Serve
lógica implementada "se o manipulador for igual nil
, use DefaultServeMux
". A aceitação nil
de um parâmetro pode levar o chamador a pensar que pode ser passado nil
para ambos os parâmetros. Mas talServe
http.Serve(nil, nil)
leva a um pânico terrível.Dica . Não misture parâmetros na mesma assinatura de função nil
e não nil
.
O autor http.ListenAndServe
tentou simplificar a vida dos usuários da API para o caso padrão, mas a segurança foi afetada.Na presença, nil
não há diferença no número de linhas entre uso explícito e indireto DefaultServeMux
. const root = http.Dir("/htdocs") http.Handle("/", http.FileServer(root)) http.ListenAndServe("0.0.0.0:8080", nil)
comparado com const root = http.Dir("/htdocs") http.Handle("/", http.FileServer(root)) http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)
Valeu a confusão manter uma linha? const root = http.Dir("/htdocs") mux := http.NewServeMux() mux.Handle("/", http.FileServer(root)) http.ListenAndServe("0.0.0.0:8080", mux)
Dica . Pense seriamente em quanto tempo as funções auxiliares salvarão o programador. Clareza é melhor que concisão.
Dica . Evite APIs públicas com parâmetros que somente os testes precisam. Evite exportar APIs com parâmetros cujos valores diferem apenas durante o teste. Em vez disso, as funções do wrapper de exportação que ocultam a transferência desses parâmetros e nos testes usam funções auxiliares semelhantes que passam os valores necessários para o teste.
6.2.2 Use argumentos de comprimento variável em vez de [] T
Muitas vezes, uma função ou método usa uma fatia dos valores. func ShutdownVMs(ids []string) error
Este é apenas um exemplo inventado, mas isso é muito comum. O problema é que essas assinaturas assumem que serão chamadas com mais de um registro. Como mostra a experiência, eles geralmente são chamados com apenas um argumento, que deve ser "empacotado" dentro da fatia para atender aos requisitos da assinatura da função.Além disso, como o parâmetro ids
é uma fatia, você pode passar uma fatia vazia ou zero para a função e o compilador ficará feliz. Isso adiciona uma carga extra de teste, pois o teste deve cobrir esses casos.Para dar um exemplo dessa classe de API, refatorei recentemente a lógica que exigia a instalação de alguns campos adicionais se pelo menos um dos parâmetros fosse diferente de zero. A lógica era algo assim: if svc.MaxConnections > 0 || svc.MaxPendingRequests > 0 || svc.MaxRequests > 0 || svc.MaxRetries > 0 {
Como o operador estava if
ficando muito longo, eu queria colocar a lógica de validação em uma função separada. Aqui está o que eu vim com:
Isso tornou possível indicar claramente a condição sob a qual a unidade interna será executada: if anyPositive(svc.MaxConnections, svc.MaxPendingRequests, svc.MaxRequests, svc.MaxRetries) {
No entanto, há um problema com anyPositive
, alguém poderia acidentalmente chamá-lo assim: if anyPositive() { ... }
Nesse caso, anyPositive
retornará false
. Esta não é a pior opção. Pior se anyPositive
retornado true
na ausência de argumentos.No entanto, seria melhor poder alterar a assinatura de anyPositive para garantir que pelo menos um argumento seja passado ao chamador. Isso pode ser feito combinando parâmetros para argumentos normais e argumentos de comprimento variável (varargs):
Agora anyPositive
você não pode chamar com menos de um argumento.6.3 Deixe as funções determinarem o comportamento desejado.
Suponha que me foi atribuída a tarefa de escrever uma função que preserva a estrutura Document
no disco.
Eu poderia escrever uma função Save
que grava Document
em um arquivo *os.File
. Mas existem alguns problemas.A assinatura Save
elimina a possibilidade de gravar dados pela rede. Se esse requisito aparecer no futuro, a assinatura da função precisará ser alterada, o que afetará todos os objetos de chamada.Save
Também é desagradável para o teste, uma vez que trabalha diretamente com os arquivos no disco. Portanto, para verificar seu funcionamento, o teste deve ler o conteúdo do arquivo após a gravação.E eu tenho que garantir que ele seja f
gravado em uma pasta temporária e posteriormente excluído.*os.File
também define muitos métodos que não estão relacionados a Save
, por exemplo, ler diretórios e verificar se um caminho é um link simbólico. Bem, se a assinaturaSave
descreveu apenas as partes relevantes *os.File
.O que pode ser feito?
Com a ajuda io.ReadWriteCloser
dele, você pode aplicar o princípio de separação da interface - e redefini-lo Save
em uma interface que descreva as propriedades mais gerais do arquivo.Após essa alteração, qualquer tipo que implemente a interface io.ReadWriteCloser
pode ser substituído pelo anterior *os.File
.Isso expande simultaneamente o escopo Save
e esclarece ao chamador quais métodos de tipo *os.File
estão relacionados à sua operação.E o autor Save
não pode mais chamar esses métodos não relacionados *os.File
, porque ele está oculto por trás da interface io.ReadWriteCloser
.Mas podemos estender o princípio da separação de interface ainda mais.Em primeiro lugar seSave
segue o princípio da responsabilidade única, é improvável que ele leia o arquivo que acabou de escrever para verificar seu conteúdo - outro código deve fazer isso.
Portanto, você pode restringir as especificações da interface para Save
apenas escrever e fechar.Em segundo lugar, o mecanismo de fechamento do encadeamento y Save
é um legado do tempo em que trabalhou com o arquivo. A questão é: em que circunstâncias wc
ele será fechado.Se a Save
causa Close
incondicionalmente, quer no caso de sucesso.Isso apresenta um problema para o chamador, pois ele pode querer adicionar dados ao fluxo após a gravação do documento.
A melhor opção é redefinir Salvar para trabalhar apenas io.Writer
, salvando o operador de todas as outras funcionalidades, exceto para gravar dados no fluxo.Depois de aplicar o princípio de separação de interface, a função tornou-se ao mesmo tempo mais específica em termos de requisitos (precisa apenas de um objeto onde possa ser gravada) e mais geral em termos de funcionalidade, já que agora podemos usá-la Save
para salvar dados onde quer que seja implementada io.Writer
.7. Tratamento de erros
Fiz várias apresentações e escrevi muito sobre esse tópico no blog, então não vou repeti-lo. Em vez disso, quero cobrir duas outras áreas relacionadas ao tratamento de erros.7.1 Elimine a necessidade de tratamento de erros removendo os próprios erros
Fiz muitas sugestões para melhorar a sintaxe de manipulação de erros, mas a melhor opção é não lidar com elas.Nota . Não digo "excluir tratamento de erros". Sugiro alterar o código para que não haja erros no processamento.
O recente livro de filosofia de desenvolvimento de software de John Osterhout me inspirou a fazer essa sugestão . Um dos capítulos é intitulado "Eliminar erros da realidade". Vamos tentar aplicar este conselho.7.1.1 Contagem de linhas
Escreveremos uma função para contar o número de linhas em um arquivo. func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err = br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, err } return lines, nil }
Conforme seguimos o conselho das seções anteriores, CountLines
aceita io.Reader
, não *os.File
; já é tarefa do chamador fornecer io.Reader
cujo conteúdo queremos contar.Criamos bufio.Reader
e, em seguida, chamamos o método em um loop ReadString
, aumentando o contador, até chegarmos ao final do arquivo e retornamos o número de linhas lidas.Pelo menos queremos escrever esse código, mas a função está sobrecarregada com o tratamento de erros. Por exemplo, há uma construção tão estranha: _, err = br.ReadString('\n') lines++ if err != nil { break }
Aumentamos o número de linhas antes de verificar se há erros - isso parece estranho.A razão pela qual devemos escrever dessa maneira é porque ele ReadString
retornará um erro se encontrar o final do arquivo antes do caractere de nova linha. Isso pode acontecer se não houver nova linha no final do arquivo.Para tentar corrigir isso, altere a lógica do contador de linhas e veja se precisamos sair do loop.Nota . Essa lógica ainda não é perfeita, você pode encontrar um erro?
Mas ainda não terminamos de verificar se há erros. ReadString
retornará io.EOF
quando encontrar o final do arquivo. Essa é a situação esperada, portanto, para ReadString
você precisar fazer alguma maneira de dizer "pare, não há mais nada para ler". Portanto, antes de retornar o erro ao objeto de chamada CountLine
, é necessário verificar se o erro não está relacionado io.EOF
e depois transmiti-lo; caso contrário, retornamos nil
e dizemos que está tudo bem.Penso que este é um bom exemplo da tese de Russ Cox sobre como o tratamento de erros pode ocultar a função. Vejamos a versão melhorada. func CountLines(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() }
Esta versão aprimorada usa em seu bufio.Scanner
lugar bufio.Reader
.Sob o capô bufio.Scanner
usa bufio.Reader
, mas adiciona um bom nível de abstração, o que ajuda a remover o tratamento de erros.. bufio.Scanner
, .
O método sc.Scan()
retornará um valor true
se o scanner encontrou uma sequência e não encontrou um erro. Portanto, o corpo do loop é for
chamado apenas se houver uma linha de texto no buffer do scanner. Isso significa que o novo CountLines
lida com casos quando não há nova linha ou quando o arquivo está vazio.Em segundo lugar, como sc.Scan
retorna false
quando um erro é detectado, o ciclo for
termina quando atinge o final do arquivo ou um erro é detectado. O tipo bufio.Scanner
lembra o primeiro erro encontrado e, usando o método sc.Err()
, podemos restaurar esse erro assim que sairmos do loop.Por fim, ele sc.Err()
cuida do processamento io.EOF
e o converte para nil
se o final do arquivo for alcançado sem erros.Dica . Se você encontrar um tratamento excessivo de erros, tente extrair algumas operações para um tipo auxiliar.
7.1.2 Writeresponse
Meu segundo exemplo é inspirado no post "Erros são valores" .Anteriormente, vimos exemplos de como um arquivo é aberto, gravado e fechado. Há manipulação de erros, mas não é demais, porque as operações podem ser encapsuladas em auxiliares, como ioutil.ReadFile
e ioutil.WriteFile
. Porém, ao trabalhar com protocolos de rede de baixo nível, é necessário criar uma resposta diretamente usando as primitivas de E / S. Nesse caso, a manipulação de erros pode se tornar intrusiva. Considere um fragmento de um servidor HTTP que cria uma resposta HTTP. type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprint(w, "\r\n"); err != nil { return err } _, err = io.Copy(w, body) return err }
Primeiro, construa a barra de status fmt.Fprintf
e verifique o erro. Então, para cada cabeçalho, escrevemos um valor de chave e cabeçalho, sempre verificando um erro. Por fim, concluímos a seção do cabeçalho com outra \r\n
, verificamos o erro e copiamos o corpo da resposta para o cliente. Finalmente, embora não seja necessário verificar o erro io.Copy
, precisamos convertê-lo de dois valores de retorno para o único que retornar WriteResponse
.Isso é muito trabalho monótono. Mas você pode facilitar sua tarefa aplicando um pequeno tipo de wrapper errWriter
.errWriter
satisfaz o contrato io.Writer
, para que possa ser usado como invólucro. errWriter
passa registros pela função até que um erro seja detectado. Nesse caso, ele rejeita as entradas e retorna o erro anterior. type errWriter struct { io.Writer err error } func (e *errWriter) Write(buf []byte) (int, error) { if e.err != nil { return 0, e.err } var n int n, e.err = e.Writer.Write(buf) return n, nil } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWriter{Writer: w} fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) } fmt.Fprint(ew, "\r\n") io.Copy(ew, body) return ew.err }
Se você aplicar errWriter
para WriteResponse
, a clareza de código melhorou significativamente. Você não precisa mais verificar se há erros em cada operação individual. A mensagem de erro é movida para o final da função como uma verificação de campo ew.err
, evitando a tradução irritante dos valores io.Copy retornados.7.2 Manipule o erro apenas uma vez
Finalmente, quero observar que os erros devem ser tratados apenas uma vez. Processar significa verificar o significado do erro e tomar uma única decisão.
Se você tomar menos de uma decisão, ignorará o erro. Como vemos aqui, o erro de é w.WriteAll
ignorado.Mas tomar mais de uma decisão em resposta a um erro também está errado. Abaixo está o código que geralmente encontro. func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { log.Println("unable to write:", err)
Neste exemplo, se ocorrer um erro durante o tempo w.Write
, a linha será gravada no log e também retornada ao chamador, que também poderá registrá-lo e transmiti-lo, até o nível superior do programa.Provavelmente, o chamador faz o mesmo: func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { log.Printf("could not marshal config: %v", err) return err } if err := WriteAll(w, buf); err != nil { log.Println("could not write config: %v", err) return err } return nil }
Assim, uma pilha de linhas repetidas é criada no log. unable to write: io.EOF could not write config: io.EOF
Mas na parte superior do programa você recebe um erro original sem nenhum contexto. err := WriteConfig(f, &conf) fmt.Println(err)
Quero analisar este tópico com mais detalhes, porque não considero o problema de retornar um erro simultaneamente e registrar minhas preferências pessoais. func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { log.Printf("could not marshal config: %v", err)
Costumo encontrar um problema que um programador esquece de retornar de um erro. Como dissemos anteriormente, o estilo de Go é usar operadores de limite, verificar os pré-requisitos à medida que a função é executada e retornar mais cedo.Neste exemplo, o autor verificou o erro, registrou-o, mas esqueceu de retornar. Por isso, surge um problema sutil.O contrato de tratamento de erros Go diz que, na presença de um erro, nenhuma suposição pode ser feita sobre o conteúdo de outros valores de retorno. Como o empacotamento JSON falhou, o conteúdo é buf
desconhecido: ele pode conter nada, mas pior, pode conter um fragmento JSON semi-escrito.Como o programador esqueceu de retornar após verificar e registrar o erro, o buffer danificado será transferido WriteAll
. É provável que a operação tenha êxito e, portanto, o arquivo de configuração não será gravado corretamente. No entanto, a função é concluída normalmente, e o único sinal de que ocorreu um problema é uma linha no log em que o empacotamento JSON falhou e não uma falha no registro de configuração.7.2.1 Adicionando contexto a erros
Ocorreu um erro porque o autor estava tentando adicionar contexto à mensagem de erro. Ele tentou deixar uma marca para indicar a fonte do erro.Vejamos outra maneira de fazer o mesmo fmt.Errorf
. func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { return fmt.Errorf("could not marshal config: %v", err) } if err := WriteAll(w, buf); err != nil { return fmt.Errorf("could not write config: %v", err) } return nil } func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { return fmt.Errorf("write failed: %v", err) } return nil }
Se você combinar o registro de erro com o retorno em uma linha, é mais difícil esquecer de retornar e evitar a continuação acidental.Se ocorrer um erro de E / S durante a gravação do arquivo, o método Error()
produzirá algo como isto: could not write config: write failed: input/output error
7.2.2 Erro ao agrupar com github.com/pkg/errors
O padrão fmt.Errorf
funciona bem para registrar mensagens de erro, mas o tipo de erro segue o caminho. Argumentei que o tratamento de erros como valores opacos é importante para projetos fracamente acoplados ; portanto, o tipo de erro de origem não deve importar se precisamos apenas trabalhar com seu valor:- Verifique se não é zero.
- Exibi-lo na tela ou registrá-lo.
No entanto, acontece que você precisa restaurar o erro original. Para anotar esses erros, você pode usar algo como o meu pacote errors
: func ReadFile(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, errors.Wrap(err, "open failed") } defer f.Close() buf, err := ioutil.ReadAll(f) if err != nil { return nil, errors.Wrap(err, "read failed") } return buf, nil } func ReadConfig() ([]byte, error) { home := os.Getenv("HOME") config, err := ReadFile(filepath.Join(home, ".settings.xml")) return config, errors.WithMessage(err, "could not read config") } func main() { _, err := ReadConfig() if err != nil { fmt.Println(err) os.Exit(1) } }
Agora a mensagem se torna um belo bug no estilo K & D: could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
e seu valor contém um link para o motivo original. func main() { _, err := ReadConfig() if err != nil { fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err)) fmt.Printf("stack trace:\n%+v\n", err) os.Exit(1) } }
Assim, você pode restaurar o erro original e exibir o rastreamento da pilha: original error: *os.PathError open /Users/dfc/.settings.xml: no such file or directory stack trace: open /Users/dfc/.settings.xml: no such file or directory open failed main.ReadFile /Users/dfc/devel/practical-go/src/errors/readfile2.go:16 main.ReadConfig /Users/dfc/devel/practical-go/src/errors/readfile2.go:29 main.main /Users/dfc/devel/practical-go/src/errors/readfile2.go:35 runtime.main /Users/dfc/go/src/runtime/proc.go:201 runtime.goexit /Users/dfc/go/src/runtime/asm_amd64.s:1333 could not read config
O pacote errors
permite adicionar contexto aos valores de erro em um formato conveniente para uma pessoa e uma máquina. Em uma apresentação recente, eu lhe disse que no próximo lançamento do Go, esse invólucro aparecerá na biblioteca padrão.8. Concorrência
O Go geralmente é escolhido por causa de seus recursos de concorrência. Os desenvolvedores fizeram muito para aumentar sua eficiência (em termos de recursos de hardware) e desempenho, mas as funções de paralelismo da Go podem ser usadas para escrever código que não é produtivo nem confiável. No final do artigo, quero dar algumas dicas sobre como evitar algumas das armadilhas das funções simultâneas do Go.O suporte de simultaneidade de primeira linha da Go é fornecido pelos canais, bem como instruções select
ego
. Se você estudou a teoria do Go em livros didáticos ou em uma universidade, deve ter notado que a seção paralelismo é sempre uma das últimas do curso. Nosso artigo não é diferente: decidi falar sobre paralelismo no final, como algo adicional às habilidades usuais que o programador do Go deve aprender.Há uma certa dicotomia aqui, porque a principal característica do Go é o nosso modelo simples e fácil de paralelismo. Como produto, nossa linguagem se vende à custa de quase essa função. Por outro lado, a concorrência não é realmente tão fácil de usar; caso contrário, os autores não o tornariam o último capítulo de seus livros e não teríamos visto com pesar nosso código.Esta seção discute algumas das armadilhas do uso ingênuo das funções de simultaneidade Go.8.1 Faça algum trabalho o tempo todo.
Qual é o problema com este programa? package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() for { } }
O programa faz o que pretendemos: serve um servidor web simples. Ao mesmo tempo, gasta o tempo da CPU em um loop infinito, porque for{}
na última linha ele main
bloqueia o gorutin main, sem executar nenhuma E / S, não há espera para bloquear, enviar ou receber mensagens ou algum tipo de conexão com o sheduler.Como o tempo de execução do Go geralmente é atendido por um sheduler, esse programa é executado sem sentido no processador e pode terminar em um bloqueio ativo (live-lock).Como consertar isso? Aqui está uma opção. package main import ( "fmt" "log" "net/http" "runtime" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() for { runtime.Gosched() } }
Pode parecer bobagem, mas essa é uma solução comum que me ocorre na vida real. Este é um sintoma de um mal-entendido do problema subjacente.Se você é um pouco mais experiente com o Go, pode escrever algo assim. package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() select {} }
Uma declaração vazia é select
bloqueada para sempre. Isso é útil, porque agora não giramos o processador inteiro apenas para uma chamada runtime.GoSched()
. No entanto, tratamos apenas o sintoma, não a causa.Quero lhe mostrar outra solução que, espero, já tenha ocorrido a você. Em vez de executar http.ListenAndServe
na goroutine, deixando o principal problema da goroutine, basta executar http.ListenAndServe
na goroutine principal.Dica . Se você sair da função main.main
, o programa Go será encerrado incondicionalmente, independentemente do que outras goroutines em execução durante a execução do programa façam.
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
Portanto, este é o meu primeiro conselho: se a goroutine não puder progredir até que ele receba um resultado de outro, então é mais fácil fazer o trabalho sozinho, do que delegá-lo.Isso geralmente elimina muitos rastreamentos de estados e manipulação de canais necessários para transferir o resultado da goroutina para o iniciador do processo.Dica . Muitos programadores do Go abusam de goroutines, especialmente no início. Como tudo na vida, a chave do sucesso é a moderação.
8.2 Deixe o paralelismo para o chamador
Qual é a diferença entre as duas APIs?
Mencionamos as diferenças óbvias: o primeiro exemplo lê o diretório em uma fatia e, em seguida, retorna a fatia ou o erro inteiro se algo der errado. Isso acontece de forma síncrona, o chamador bloqueia ListDirectory
até que todas as entradas do diretório sejam lidas. Dependendo do tamanho do diretório, pode levar muito tempo e potencialmente muita memória.Considere o segundo exemplo. É um pouco mais como a programação Go clássica, aqui ListDirectory
retorna o canal através do qual as entradas do diretório serão transmitidas. Quando o canal está fechado, isso é um sinal de que não há mais entradas no catálogo. Como o preenchimento do canal ocorre após o retorno ListDirectory
, pode-se assumir que as goroutines começam a preencher o canal.Nota . Na segunda opção, não é necessário usar a goroutine: você pode selecionar um canal suficiente para armazenar todas as entradas do diretório sem bloquear, preenchê-lo, fechá-lo e devolvê-lo ao chamador. Mas isso é improvável, pois nesse caso os mesmos problemas surgirão ao usar uma grande quantidade de memória para armazenar em buffer todos os resultados no canal.
A versão do ListDirectory
canal tem mais dois problemas:- O uso de um canal fechado como um sinal que não há mais elementos a serem processados
ListDirectory
não pode informar o chamador de um conjunto incompleto de elementos devido a um erro. O chamador não tem como transmitir a diferença entre um diretório vazio e um erro. Nos dois casos, parece que o canal será imediatamente fechado.
- O chamador deve continuar lendo o canal quando estiver fechado, porque esta é a única maneira de entender que a goroutina de preenchimento do canal parou de funcionar. Essa é uma restrição séria ao uso
ListDirectory
: o chamador passa um tempo lendo do canal, mesmo que tenha recebido todos os dados necessários. Isso provavelmente é mais eficiente em termos de uso de memória para diretórios médios e grandes, mas o método não é mais rápido que o método baseado em fatia original.
Nos dois casos, a solução é usar um retorno de chamada: uma função que é chamada no contexto de cada entrada de diretório enquanto é executada. func ListDirectory(dir string, fn func(string))
Sem surpresa, a função filepath.WalkDir
funciona dessa maneira.Dica . Se sua função iniciar a goroutine, você deverá fornecer ao chamador uma maneira de interromper explicitamente essa rotina. Geralmente, é mais fácil deixar o modo de execução assíncrona no chamador.
8.3 Nunca execute a goroutine sem saber quando vai parar
No exemplo anterior, a goroutine foi usada desnecessariamente. Mas um dos principais pontos fortes de Go são seus recursos de simultaneidade de primeira classe. De fato, em muitos casos, o trabalho paralelo é bastante apropriado e é necessário o uso de goroutines.Esse aplicativo simples atende o tráfego http em duas portas diferentes: porta 8080 para tráfego de aplicativos e porta 8001 para acesso ao terminal /debug/pprof
. package main import ( "fmt" "net/http" _ "net/http/pprof" ) func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) go http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
Embora o programa seja simples, é a base de um aplicativo real.O aplicativo em sua forma atual tem vários problemas que aparecerão à medida que crescem, então vamos examinar imediatamente alguns deles. func serveApp() { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) http.ListenAndServe("0.0.0.0:8080", mux) } func serveDebug() { http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) } func main() { go serveDebug() serveApp() }
manipuladores de quebra serveApp
e serveDebug
de funções distintas, temos os separou main.main
. Nós também seguiu o conselho anterior e fez com que serveApp
e serveDebug
deixar a tarefa para garantir o paralelismo do chamador.Mas existem alguns problemas com o desempenho desse programa. Se sairmos serveApp
e sairmos main.main
, o programa será encerrado e será reiniciado pelo gerenciador de processos.Dica . Assim como as funções em Go deixam paralelismo para o chamador, os aplicativos devem parar de monitorar seu estado e reiniciar o programa que os chamou. Não responsabilize seus aplicativos por reiniciarem: este procedimento é melhor manipulado de fora do aplicativo.
No entanto, ele serveDebug
inicia em uma goroutina separada e, no caso de seu lançamento, a goroutine termina, enquanto o restante do programa continua. Seus desenvolvedores não gostarão do fato de que você não pode obter estatísticas de aplicativos porque o manipulador /debug
parou de funcionar.Precisamos garantir que o aplicativo seja fechado se alguma goroutina que o servir parar . func serveApp() { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) if err := http.ListenAndServe("0.0.0.0:8080", mux); err != nil { log.Fatal(err) } } func serveDebug() { if err := http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux); err != nil { log.Fatal(err) } } func main() { go serveDebug() go serveApp() select {} }
Agora serverApp
eles serveDebug
verificam os erros ListenAndServe
e, se necessário, os chamam log.Fatal
. Como os dois manipuladores trabalham em goroutines, traçamos a rotina principal em select{}
.Essa abordagem tem vários problemas:- Se ele
ListenAndServe
retornar com um erro nil
, não haverá chamada log.Fatal
e o serviço HTTP nessa porta será encerrado sem parar o aplicativo.
log.Fatal
chama os.Exit
que sai incondicionalmente do programa; chamadas adiadas não funcionarão, outras goroutines não serão notificadas do fechamento, o programa simplesmente parará. Isso dificulta a gravação de testes para essas funções.
Dica . Use apenas log.Fatal
em funções main.main
ou init
.
De fato, queremos transmitir qualquer erro que ocorra ao criador da goroutina, para que ele possa descobrir por que ela parou e concluiu o processo de maneira limpa. func serveApp() error { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) return http.ListenAndServe("0.0.0.0:8080", mux) } func serveDebug() error { return http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) } func main() { done := make(chan error, 2) go func() { done <- serveDebug() }() go func() { done <- serveApp() }() for i := 0; i < cap(done); i++ { if err := <-done; err != nil { fmt.Println("error: %v", err) } } }
O status de retorno da Goroutine pode ser obtido através do canal. O tamanho do canal é igual ao número de goroutines que queremos controlar, portanto, o envio ao canal done
não será bloqueado, pois isso bloqueará o desligamento das goroutines e causará um vazamento.Como o canal done
não pode ser fechado com segurança, não podemos usar o idioma para o ciclo do canal for range
até que todas as goroutines tenham sido relatadas. Em vez disso, executamos todas as goroutines em execução em um ciclo, que é igual à capacidade do canal.Agora temos uma maneira de sair corretamente de todas as goroutines e corrigir todos os erros que encontrarem. Resta apenas enviar um sinal para concluir o trabalho da primeira goroutina para todos os outros.O apelo ahttp.Server
sobre a conclusão, envolvi essa lógica em uma função auxiliar. O ajudante serve
aceita o endereço e http.Handler
, da mesma forma http.ListenAndServe
, o canal stop
que usamos para executar o método Shutdown
. func serve(addr string, handler http.Handler, stop <-chan struct{}) error { s := http.Server{ Addr: addr, Handler: handler, } go func() { <-stop
Agora, para cada valor no canal done
, fechamos o canal stop
, o que faz com que cada gorutina desse canal se feche http.Server
. Por sua vez, isso leva a um retorno de todas as goroutinas restantes ListenAndServe
. Quando todas as gorutinas em execução são interrompidas, main.main
o processo termina e o processo é interrompido corretamente.Dica . Escrever essa lógica por conta própria é um trabalho repetitivo e o risco de erros. Veja algo como este pacote que fará a maior parte do trabalho para você.