Ao criar um pacote em Go, normalmente, o objetivo final é tornar o pacote acessível para uso por outros desenvolvedores, seja em pacotes de ordem mais elevada ou em programas inteiros. Ao importar o pacote, seu código poderá servir como o 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.
Neste contexto, a visibilidade representa o espaço do arquivo a partir do qual é possível referenciar um pacote ou outro contructo. Por exemplo, se definirmos uma variável em uma função, a visibilidade (escopo) daquela variável fica apenas dentro da função na qual ela foi definida. Analogamente, se você definir uma variável em um pacote, você poderá torná-la visível apenas para aquele pacote ou permitir que ela fique visível também fora do pacote.
Desse modo, é importante controlar cuidadosamente da visibilidade do pacote ao se escrever códigos ergonômicos, especialmente levando-se em conta as futuras alterações que possam vir a ser feitas no pacote. Se você precisar corrigir um erro, melhorar o desempenho ou alterar funcionalidades, você irá querer fazer a mudança de maneira a não quebrar o código de quem estiver usando o seu pacote. Uma maneira de minimizar alterações problemáticas é permitir o acesso somente às partes do pacote necessárias para que seja usado corretamente. Ao limitar o acesso, você pode alterar o pacote internamente, com menor chance de afetar a forma como outros desenvolvedores o estão usando.
Neste artigo, você aprenderá como controlar a visibilidade de pacotes, além de como proteger as partes do seu código que devem ser usadas apenas dentro do seu pacote. Para fazer isso, vamos criar um agente básico para registrar e depurar mensagens, usando pacotes com graus diferentes de visibilidade dos itens.
Para seguir os exemplos neste artigo, será necessário:
.
├── bin
│
└── src
└── github.com
└── gopherguides
Ao contrário do que ocorre em outras linguagens de programação,como Java e Python - que usam modificadores de acesso, como public
, private
ou protected
para especificar o escopo, a linguagem Go determina se um item é exported
[exportado] e unexported
[não exportado] pela forma como é declarado. Exportar um item neste caso torna ele visible
[visível] fora do pacote atual. Caso não seja exportado, ele fica apenas visível e utilizável de dentro do pacote em que foi definido.
Esta visibilidade externa é controlada colocando-se a primeira letra do item declarado em maiúscula. Todas as declarações, tais como Types
, Variables
, Constants
, Functions
etc., que começam com uma letra maiúscula, ficam visíveis fora do pacote atual.
Vejamos o código a seguir, prestando muita atenção ao uso de letras maiúsculas e minúsculas:
package greet
import "fmt"
var Greeting string
func Hello(name string) string {
return fmt.Sprintf(Greeting, name)
}
Esse código declara que ele está no pacote greet
. Depois, ele declara dois símbolos, uma variável chamada Greeting
e uma função chamada Hello
. Como ambas começam com uma letra maiúscula, as duas são exported
e disponibilizadas para qualquer programa exterior. Como indicado anteriormente, a criação de um pacote que limita o acesso permitirá um melhor design da API, além de facilitar a atualização do seu pacote internamente sem quebrar o código de quem estiver contando com o seu pacote.
Para examinar melhor como a visibilidade de pacotes funciona em um programa, vamos criar um pacote logging
, tendo em mente o que queremos tornar visível fora do nosso pacote e o que não vamos tornar visível. Esse pacote de registro será responsável por registrar todas as mensagens de nosso programa no console. Ele também examinará em qual nível estamos registrando. Um nível descreve o tipo de registro e tem um destes três status: info
, warning
ou error
.
Primeiro, dentro do seu diretório src
, vamos criar um diretório chamado logging
, onde colocaremos nossos arquivos de registro:
- mkdir logging
Acesse aquele diretório em seguida:
- cd logging
Então, usando um editor como o nano, crie um arquivo chamado logging.go
:
- nano logging.go
Coloque o código a seguir no arquivo logging.go
que acabamos de criar:
package logging
import (
"fmt"
"time"
)
var debug bool
func Debug(b bool) {
debug = b
}
func Log(statement string) {
if !debug {
return
}
fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement)
}
A primeira linha desse código declarou um pacote chamado logging
. Nesse pacote, há duas funções exported
: Debug
e Log
. Essas funções podem ser chamadas por qualquer outro pacote que importe o pacote logging
. Também há uma variável privada chamada debug
. Essa variável é acessível apenas de dentro do pacote logging
. É importante notar que, apesar da função Debug
e da variável debug
terem a mesma grafia, a função tem letra maíuscula e a variável não. Isso as torna declarações distintas com escopos diferentes.
Salve e saia do arquivo.
Para usar esse pacote em outras áreas do nosso código, podemos usar o import
nele em um novo pacote. Criaremos esse novo pacote, mas vamos precisar de um novo diretório para armazenar esses arquivos fonte em primeiro lugar.
Vamos sair do diretório logging
, criar um novo diretório chamado cmd
e ir até esse novo diretório:
- cd ..
- mkdir cmd
- cd cmd
Crie um arquivo chamado main.go
no diretório cmd
que acabamos de criar:
- nano main.go
Agora, podemos adicionar o seguinte código:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
}
Agora, temos nosso programa escrito por completo. No entanto, antes de executarmos este programa, também precisaremos criar alguns dos arquivos de configuração para que o nosso código funcione corretamente. A linguagem Go usa Módulos Go para configurar as dependências de pacotes para a importação de recursos. Os módulos Go são arquivos de configuração colocados no seu diretório de pacotes que dizem ao compilador de onde importar os pacotes. Embora o aprendizado acerca dos módulos ultrapasse o âmbito deste artigo, podemos escrever apenas algumas linhas de configuração para fazer com que este exemplo funcione localmente.
Abra o arquivo go.mod
no diretório cmd
:
- nano go.mod
Então, coloque o seguinte conteúdo no arquivo:
module github.com/gopherguides/cmd
replace github.com/gopherguides/logging => ../logging
A primeira linha desse arquivo diz ao compilador que o pacote cmd
tem o caminho de arquivo github.com/gopherguides/cmd
. A segunda linha diz ao compilador que o pacote github.com/gopherguides/logging
pode ser encontrado localmente em disco, no diretório ../logging
.
Também precisaremos de um arquivo go.mod
para o nosso pacote logging
. Vamos voltar ao diretório logging
e criar um arquivo go.mod
:
- cd ../logging
- nano go.mod
Adicione o conteúdo a seguir ao arquivo:
module github.com/gopherguides/logging
Isso diz ao compilador que o pacote logging
que criamos é, na verdade, o pacote github.com/gopherguides/logging
. Isso torna possível importar o pacote no 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ório e layout de arquivo:
├── cmd
│ ├── go.mod
│ └── main.go
└── logging
├── go.mod
└── logging.go
Agora que temos toda a configuração completa, podemos executar o programa main a
partir do pacote cmd
, com os seguintes comandos:
- cd ../cmd
- go run main.go
Você irá obter um resultado similar ao seguinte:
Output2019-08-28T11:36:09-05:00 This is a debug statement...
O programa imprimirá o tempo atual no formato RFC 3339, seguido de qualquer instrução que tivermos enviado ao agente de log. RFC 3339 é um formato de hora que foi concebido para representar a hora na internet e é comumente usado em arquivos de registro.
Como as funções Debug
e Log
são exportadas do pacote logging, podemos usá-las em nosso pacote main
. No entanto, a variável debug
no pacote logging
não será exportada. A tentativa de referenciar uma declaração não exportada resultará em um erro de tempo de compilação.
Adicione a seguinte linha destacada ao 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 similar ao seguinte:
Output. . .
./main.go:10:14: cannot refer to unexported name logging.debug
Agora que vimos como se comportam os itens exported
e unexported
dos pacotes, vamos examinar, a seguir, como os fields
[campos] e methods
[métodos] podem ser exportados de structs
.
Embora o esquema de visibilidade no agente de log - que construímos na última seção - possa funcionar para programas simples, ele compartilha estados demais para ser útil dentro de vários pacotes. Isso acontece porque as variáveis exportadas ficam acessíveis a vários pacotes que poderiam modificar as variáveis em estados contraditórios. Permitir que o estado do seu pacote seja alterado dessa maneira torna difícil prever como seu programa se comportará. Com o design atual, por exemplo, um pacote poderia definir a variável Debug
como true
[verdadeiro] e outro poderia defini-la como false
[falso] na mesma instância. Isso criaria um problema, uma vez que ambos os pacotes que estão importando o pacote logging
são afetados.
Podemos deixar o agente de log isolado, criando uma struct e, na sequência, recuando os métodos para longe dela. Isso nos permitirá criar uma instance
[instância] de um agente de log a ser usada de maneira independente em cada pacote que a consome.
Altere o pacote logging
para o seguinte, no intuito de refatorar o código e isolar o agente de log.
package logging
import (
"fmt"
"time"
)
type Logger struct {
timeFormat string
debug bool
}
func New(timeFormat string, debug bool) *Logger {
return &Logger{
timeFormat: timeFormat,
debug: debug,
}
}
func (l *Logger) Log(s string) {
if !l.debug {
return
}
fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s)
}
Neste código, criamos uma struct chamada Logger
. Ela irá abrigar nosso estado não exportado, incluindo o formato de hora a ser impresso e a definição da variável debug
como true
ou false
. A função New
define o estado inicial para usar na criação do agente de log, como o formato de hora e o estado de depuração. Então, ele armazena os valores que demos a ele internamente para as variáveis não exportadas timeFormat
e debug
. Também criamos um método chamado Log
no tipo Logger
que usa uma instrução que queremos imprimir. Dentro do método Log
há uma referência à sua variável de método local l
para retomar o acesso aos seus campos internos, como a l.timeFormat
e l.debug
.
Essa abordagem nos permitirá criar uma struct Logger
em diversos pacotes e usá-la independentemente da forma como os outros pacotes a estiverem usando.
Para usá-la em outro pacote, vamos alterar o cmd/main.go
para que se pareça com o seguinte:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, true)
logger.Log("This is a debug statement...")
}
Ao executar esse programa, você terá o seguinte resultado:
Output2019-08-28T11:56:49-05:00 This is a debug statement...
Nesse código, criamos uma instância do agente de log ao chamar a função exportada New
. Armazenamos a referência a essa instância na variável logger
. Agora, podemos chamar logging.Log
para imprimir as instruções.
Se tentarmos referenciar um campo não exportado do Logger
, como o campo timeFormat
, receberemos um erro de tempo de compilação. Tente adicionar a seguinte linha destacada e executar o 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 dará o seguinte erro:
Output. . .
cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
O compilador reconhece que o logger.timeFormat
não foi exportado, e, desta forma, não pode ser recuperado do pacote logging
.
Assim como ocorre com os campos struct, os métodos também podem ser exportados ou não exportados.
Para ilustrar isso, vamos adicionar registro nivelado ao nosso agente de log. O registro nivelado é um meio de categorizar seus registros para que você possa procurar seus registros para tipos específicos de eventos. Os níveis que vamos colocar no nosso agente de log são:
O nível info
, que representa eventos do tipo informação que informam o usuário de uma ação, como Program started
[Programa iniciado] ou Email sent
[E-mail enviado]. Isso nos ajuda a depurar e rastrear partes do nosso programa para ver se o comportamento esperado está acontecendo.
O nível warning
[aviso]. Esses tipos de eventos identificam quando algo inesperado está acontecendo e que não é um erro, como Email failed to send, retrying
[Falha ao enviar e-mail, tentando novamente]. Eles nos ajudam a ver partes do nosso programa que não estão rodando tão bem quanto esperávamos que estivessem.
O nível error
[erro], que significa que o programa encontrou um problema, como File not found
[Arquivo não encontrado]. Frequentemente, isso resultará na falha da operação do programa.
Você também pode querer ligar e desligar certos níveis de registro, especialmente se o seu programa não estiver com o desempenho esperado e você quiser depurar o programa. Vamos adicionar essa funcionalidade, alterando o programa para que - quando debug
for definido como true
- ele imprima todos os níveis de mensagens. Caso contrário, se for false
, imprimirá apenas mensagens de erro.
Adicione o registro nivelado, fazendo as seguintes alterações em logging/logging.go
:
package logging
import (
"fmt"
"strings"
"time"
)
type Logger struct {
timeFormat string
debug bool
}
func New(timeFormat string, debug bool) *Logger {
return &Logger{
timeFormat: timeFormat,
debug: debug,
}
}
func (l *Logger) Log(level string, s string) {
level = strings.ToLower(level)
switch level {
case "info", "warning":
if l.debug {
l.write(level, s)
}
default:
l.write(level, s)
}
}
func (l *Logger) write(level string, s string) {
fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s)
}
Nesse exemplo, introduzimos um novo argumento ao método Log
. Agora, podemos enviar o level
[nível] da mensagem de registro. O método Log
determina o nível em que a mensagem está. Se for uma mensagem de status info
ou warning
e o campo debug
for true
, então, ele escreve a mensagem. Caso contrário, a mensagem é ignorada. Se estiver em qualquer outro nível, como error
, a mensagem será escrita de qualquer forma.
Grande parte da lógica - para determinar se a mensagem será impressa, encontra-se no método Log
. Também introduzimos um método não exportado chamado write
. O método write
é o que, de fato, produz o registro.
Agora, podemos usar o registro nivelado em nosso outro pacote, mudando o cmd/main.go
para que se pareça com o seguinte:
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")
}
Ao executar isso, você terá:
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, o cmd/main.go
usou o método exportado Log
.
Agora, podemos enviar o level
de cada mensagem, alterando o debug
para false
:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, false)
logger.Log("info", "starting up service")
logger.Log("warning", "no tasks found")
logger.Log("error", "exiting: no work performed")
}
Agora, veremos que apenas as mensagens do nível error
foram 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 tempo 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 vê que você está tentando referenciar algo de outro pacote - que começa com uma letra em caixa baixa, ele sabe que se trata de algo que não está exportado e, portanto, gera um erro de compilação.
O agente de log neste tutorial ilustra como podemos escrever códigos que expõem apenas as partes que queremos que outros pacotes consumam. Como controlamos quais partes do pacote ficam visíveis fora do pacote, agora conseguiremos fazer alterações no futuro sem afetar nenhum código que dependa do nosso pacote. Por exemplo, se quiséssemos desligar apenas mensagens do nível info
quando o debug
estiver definido como falso, você poderia fazer essa mudança sem afetar nenhuma outra parte da sua API. Também podemos fazer alterações na mensagem de registro para incluir mais informações com segurança, tal como o diretório do qual o programa está sendo executado.
Este artigo mostrou como compartilhar códigos entre os pacotes, ao mesmo tempo protegendo os detalhes da implementação do seu pacote. Isso permite que você exporte uma API simples que raramente será alterada a título de compatibilidade com versões anteriores, mas que permitirá alterações de maneira reservada, dentro do seu pacote e conforme necessário, para que ele funcione melhor no futuro. Tal procedimento é considerado como prática recomendada na criação de pacotes e suas APIs correspondentes.
Para aprender mais sobre os pacotes em Go, leia nossos artigos Importando pacotes em Go e Como escrever pacotes em Go, ou explore toda a nossa série de artigos sobre Como programar em Go.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!