Introdução
Ao criar um pacote em Go, o objetivo final geralmente é tornar o pacote acessível para outros desenvolvedores utilizarem, seja em pacotes de ordem superior ou em programas completos. Ao importar o pacote, seu código pode servir como um bloco de construção para outras ferramentas mais complexas. No entanto, apenas certos pacotes estão disponíveis para importação. Isso é determinado pela visibilidade do pacote.
Visibilidade neste contexto refere-se ao espaço de arquivo a partir do qual um pacote ou outro constructo pode ser referenciado. Por exemplo, se definirmos uma variável em uma função, a visibilidade (escopo) dessa variável é apenas dentro da função em que foi definida. Da mesma forma, se você definir uma variável em um pacote, pode torná-la visível apenas para aquele pacote ou permitir que ela seja visível fora do pacote também.
Controlar cuidadosamente a visibilidade dos pacotes é importante ao escrever código ergonômico, especialmente ao considerar mudanças futuras que você possa querer fazer no seu pacote. Se precisar corrigir um bug, melhorar o desempenho ou alterar a funcionalidade, você vai querer fazer a mudança de uma forma que não quebre o código de ninguém que esteja usando seu pacote. Uma maneira de minimizar mudanças disruptivas é permitir o acesso apenas às partes do seu pacote que são necessárias para seu uso adequado. Ao limitar o acesso, você pode fazer alterações internas no seu pacote com menos chances de afetar como outros desenvolvedores estão usando-o.
Neste artigo, você aprenderá como controlar a visibilidade dos pacotes, bem como como proteger partes do seu código que devem ser usadas apenas dentro do seu pacote. Para isso, criaremos um logger básico para registrar e depurar mensagens, usando pacotes com diferentes graus de visibilidade de itens.
Pré-requisitos
Para seguir os exemplos neste artigo, você precisará:
- Um espaço de trabalho Go configurado seguindo Como Instalar Go e Configurar um Ambiente de Programação Local. Este tutorial usará a seguinte estrutura de arquivos:
.
├── bin
│
└── src
└── github.com
└── gopherguides
Itens Exportados e Não Exportados
Ao contrário de outras linguagens de programação como Java e Python que utilizam modificadores de acesso como public
, private
, ou protected
para especificar o escopo, Go determina se um item é exportado
e não exportado
através de como ele é declarado. Exportar um item neste caso o torna visível
fora do pacote atual. Se não for exportado, ele só é visível e utilizável dentro do pacote em que foi definido.
Essa visibilidade externa é controlada pelo uso de letra maiúscula no primeiro caractere do item declarado. Todas as declarações, como Tipos
, Variáveis
, Constantes
, Funções
, etc., que começam com uma letra maiúscula são visíveis fora do pacote atual.
Vamos analisar o seguinte código, prestando atenção especial à capitalização:
package greet
import "fmt"
var Greeting string
func Hello(name string) string {
return fmt.Sprintf(Greeting, name)
}
Este código declara que está no pacote greet
. Em seguida, declara dois símbolos, uma variável chamada Greeting
e uma função chamada Hello
. Como ambos começam com uma letra maiúscula, eles são ambos exportados
e disponíveis para qualquer programa externo. Como mencionado anteriormente, criar um pacote que limita o acesso permitirá um melhor design da API e facilitará a atualização do seu pacote internamente sem quebrar o código de ninguém que dependa do seu pacote.
Definindo a Visibilidade do Pacote
Para dar uma olhada mais detalhada de como a visibilidade do pacote funciona em um programa, vamos criar um pacote logging
, mantendo em mente o que queremos tornar visível fora do nosso pacote e o que não tornaremos visível. Este pacote de logging será responsável por registrar qualquer mensagem do nosso programa no console. Também analisará em que nível estamos registrando. Um nível descreve o tipo de log e será um dos três status: info
, warning
ou error
.
Primeiro, dentro do seu diretório src
, vamos criar um diretório chamado logging
para colocar nossos arquivos de logging:
Em seguida, mova-se para esse diretório:
Depois, usando um editor como nano, crie um arquivo chamado logging.go
:
Coloque o seguinte código no arquivo logging.go
que acabamos de criar:
A primeira linha deste código declara um pacote chamado logging
. Neste pacote, existem duas funções exportadas
: Debug
e Log
. Essas funções podem ser chamadas por qualquer outro pacote que importe o pacote logging
. Também existe uma variável privada chamada debug
. Esta variável só é acessível de dentro do pacote logging
. É importante notar que, embora a função Debug
e a variável debug
tenham a mesma grafia, a função é capitalizada e a variável não. Isso os torna declarações distintas com escopos diferentes.
Salve e saia do arquivo.
Para usar este pacote em outras áreas do nosso código, podemos importá-lo
em um novo pacote. Criaremos este novo pacote, mas precisaremos de um novo diretório para armazenar esses arquivos de origem primeiro.
Vamos sair do diretório logging
, criar um novo diretório chamado cmd
e entrar nesse novo diretório:
Crie um arquivo chamado main.go
no diretório cmd
que acabamos de criar:
Agora podemos adicionar o seguinte código:
Agora temos todo o nosso programa escrito. No entanto, antes de podermos executar este programa, precisaremos também criar alguns arquivos de configuração para que nosso código funcione corretamente. Go usa Go Modules para configurar dependências de pacotes para importar recursos. Go modules são arquivos de configuração colocados no diretório do seu pacote que informam ao compilador onde importar pacotes. Embora aprender sobre módulos esteja além do escopo deste artigo, podemos escrever apenas algumas linhas de configuração para fazer este exemplo funcionar localmente.
Abra o seguinte arquivo go.mod
no diretório cmd
:
Em seguida, coloque o seguinte conteúdo no arquivo:
module github.com/gopherguides/cmd
replace github.com/gopherguides/logging => ../logging
A primeira linha deste arquivo informa ao compilador que o pacote cmd
tem um caminho de arquivo de github.com/gopherguides/cmd
. A segunda linha informa ao compilador que o pacote github.com/gopherguides/logging
pode ser encontrado localmente no disco no diretório ../logging
.
Também precisaremos de um arquivo go.mod
para nosso pacote logging
. Vamos voltar para o diretório logging
e criar um arquivo go.mod
:
Adicione o seguinte conteúdo ao arquivo:
module github.com/gopherguides/logging
Isso informa ao compilador que o pacote logging
que criamos é, na verdade, o pacote github.com/gopherguides/logging
. Isso torna possível importar o pacote em nosso pacote main
com a seguinte linha que escrevemos anteriormente:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
}
Agora você deve ter a seguinte estrutura de diretórios e layout de arquivos:
├── cmd
│ ├── go.mod
│ └── main.go
└── logging
├── go.mod
└── logging.go
Agora que concluímos todas as configurações, podemos executar o programa main
a partir do pacote cmd
com os seguintes comandos:
Você obterá uma saída semelhante à seguinte:
Output2019-08-28T11:36:09-05:00 This is a debug statement...
O programa exibirá a hora atual no formato RFC 3339, seguido de qualquer declaração que enviamos ao logger. RFC 3339 é um formato de hora projetado para representar a hora na internet e é comumente usado em arquivos de log.
Como as funções Debug
e Log
são exportadas do pacote de logging, podemos usá-las em nosso pacote main
. No entanto, a variável debug
no pacote logging
não é exportada. Tentar referenciar uma declaração não exportada resultará em um erro de compilação.
Adicione a seguinte linha destacada a main.go
:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
fmt.Println(logging.debug)
}
Salve e execute o arquivo. Você receberá um erro semelhante ao seguinte:
Output. . .
./main.go:10:14: cannot refer to unexported name logging.debug
Agora que vimos como os itens exported
e unexported
se comportam em pacotes, veremos a seguir como fields
e methods
podem ser exportados de structs
.
Visibilidade Dentro de Structs
Enquanto o esquema de visibilidade no logger que construímos na última seção pode funcionar para programas simples, ele compartilha muito estado para ser útil dentro de múltiplos pacotes. Isso ocorre porque as variáveis exportadas são acessíveis a múltiplos pacotes que poderiam modificar as variáveis em estados contraditórios. Permitir que o estado do seu pacote seja alterado dessa maneira dificulta a previsão de como seu programa se comportará. Com o design atual, por exemplo, um pacote poderia definir a variável Debug
como true
, e outro poderia defini-la como false
na mesma instância. Isso criaria um problema, já que ambos os pacotes que estão importando o pacote logging
seriam afetados.
Podemos tornar o logger isolado criando uma struct e, em seguida, pendurando métodos nela. Isso nos permitirá criar uma instância
de um logger para ser usada de forma independente em cada pacote que a consuma.
Altere o pacote logging
para o seguinte para refatorar o código e isolar o logger:
Neste código, criamos uma estrutura Logger
. Esta estrutura abrigará nosso estado não exportado, incluindo o formato de tempo para imprimir e a variável debug
configurada como true
ou false
. A função New
define o estado inicial para criar o logger, como o formato de tempo e o estado de depuração. Em seguida, armazena os valores fornecidos internamente nas variáveis não exportadas timeFormat
e debug
. Também criamos um método chamado Log
no tipo Logger
que recebe uma declaração que desejamos imprimir. Dentro do método Log
há uma referência à sua variável de método local l
para acessar novamente seus campos internos, como l.timeFormat
e l.debug
.
Essa abordagem nos permitirá criar um Logger
em muitos pacotes diferentes e usá-lo independentemente de como os outros pacotes estão o utilizando.
Para usá-lo em outro pacote, vamos alterar cmd/main.go
para ficar assim:
Executar este programa fornecerá a seguinte saída:
Output2019-08-28T11:56:49-05:00 This is a debug statement...
Neste código, criamos uma instância do logger chamando a função exportada New
. Armazenamos a referência a esta instância na variável logger
. Agora podemos chamar logging.Log
para imprimir declarações.
Se tentarmos referenciar um campo não exportado do Logger
, como o campo timeFormat
, receberemos um erro em tempo de compilação. Tente adicionar a seguinte linha destacada e executar cmd/main.go
:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, true)
logger.Log("This is a debug statement...")
fmt.Println(logger.timeFormat)
}
Isso resultará no seguinte erro:
Output. . .
cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
O compilador reconhece que logger.timeFormat
não é exportado e, portanto, não pode ser recuperado do pacote logging
.
Visibilidade Dentro de Métodos
Da mesma forma que os campos de struct, os métodos também podem ser exportados ou não exportados.
Para ilustrar isso, vamos adicionar logginleveled ao nosso logger. O loggin leveled é uma forma de categorizar seus logs para que você possa pesquisar seus logs por tipos específicos de eventos. Os níveis que vamos colocar em nosso logger são:
-
O nível
info
, que representa eventos do tipo informação que informam ao usuário uma ação, comoPrograma iniciado
ouEmail enviado
. Esses ajudam-nos a depurar e rastrear partes do nosso programa para ver se o comportamento esperado está ocorrendo. -
O nível
warning
. Esses tipos de eventos identificam quando algo inesperado está acontecendo e não é um erro, comoFalha no envio do email, tentando novamente
. Eles ajudam-nos a ver partes do nosso programa que não estão indo tão suavemente quanto esperávamos. -
O nível
error
, que significa que o programa encontrou um problema, comoArquivo não encontrado
. Isso frequentemente resultará na falha da operação do programa.
Você também pode desejar ativar e desativar certos níveis de logging, especialmente se seu programa não estiver performando como esperado e você deseja depurar o programa. Adicionaremos essa funcionalidade alterando o programa para que, quando debug
estiver definido como true
, ele imprimirá todos os níveis de mensagens. Caso contrário, se for false
, ele só imprimirá mensagens de erro.
Adicione logging em níveis fazendo as seguintes mudanças em logging/logging.go
:
Neste exemplo, introduzimos um novo argumento ao método Log
. Agora podemos passar o level
da mensagem de log. O método Log
determina qual nível de mensagem é. Se for uma mensagem info
ou warning
, e o campo debug
for true
, então ele escreve a mensagem. Caso contrário, ignora a mensagem. Se for qualquer outro nível, como error
, ele escreverá a mensagem independentemente.
A maior parte da lógica para determinar se a mensagem é impressa está no método Log
. Também introduzimos um método não exportado chamado write
. O método write
é o que realmente gera a mensagem de log.
Podemos agora utilizar esse registro hierárquico em nosso outro pacote, alterando cmd/main.go
para ficar assim:
Executar isso resultará em:
Output[info] 2019-09-23T20:53:38Z starting up service
[warning] 2019-09-23T20:53:38Z no tasks found
[error] 2019-09-23T20:53:38Z exiting: no work performed
Neste exemplo, cmd/main.go
usou com sucesso o método Log
exportado.
Podemos agora passar o level
de cada mensagem, alternando debug
para false
:
Agora veremos que apenas as mensagens de nível error
são impressas:
Output[error] 2019-08-28T13:58:52-05:00 exiting: no work performed
Se tentarmos chamar o método write
de fora do pacote logging
, receberemos um erro de compilação:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, true)
logger.Log("info", "starting up service")
logger.Log("warning", "no tasks found")
logger.Log("error", "exiting: no work performed")
logger.write("error", "log this message...")
}
Outputcmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)
Quando o compilador percebe que você está tentando referenciar algo de outro pacote que começa com uma letra minúscula, ele sabe que não é exportado e, portanto, gera um erro de compilação.
O logger neste tutorial ilustra como podemos escrever código que expõe apenas as partes que desejamos que outros pacotes consumam. Como controlamos quais partes do pacote são visíveis fora do pacote, agora somos capazes de fazer mudanças futuras sem afetar nenhum código que dependa do nosso pacote. Por exemplo, se quiséssemos desligar apenas mensagens de nível info
quando debug
é falso, você poderia fazer essa mudança sem afetar qualquer outra parte da sua API. Também poderíamos fazer mudanças com segurança nas mensagens de log para incluir mais informações, como o diretório de onde o programa estava sendo executado.
Conclusão
Este artigo mostrou como compartilhar código entre pacotes enquanto também protege os detalhes de implementação do seu pacote. Isso permite que você exporte uma API simples que raramente mudará para compatibilidade com versões anteriores, mas permitirá mudanças privadas em seu pacote conforme necessário para melhorar seu funcionamento no futuro. Isso é considerado uma prática recomendada ao criar pacotes e suas respectivas APIs.
Para saber mais sobre pacotes no Go, confira nossos artigos Importando Pacotes no Go e Como Escrever Pacotes no Go, ou explore nossa série completa Como Programar em Go.
Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go