This entry is parte 5 de 12 in the series Golang

No universo de Go, os pacotes são o principal mecanismo de organização e reutilização de código. Ao longo desta série, já utilizamos pacotes da biblioteca padrão — como fmtmath e time — por meio da palavra-chave import, incorporando funcionalidades prontas diretamente em nossos programas.

Esse uso, embora comum, representa apenas uma parte do modelo de organização da linguagem.

Um pacote em Go não é apenas uma coleção de funções utilitárias. Ele define um limite claro de responsabilidade, agrupando tipos, funções e comportamentos relacionados em uma unidade coesa. Essa estrutura permite que o código seja dividido em partes menores, mais fáceis de entender, manter e reutilizar.

Neste artigo, o foco é justamente esse: compreender como o Go organiza código através de pacotes. A partir dos exemplos já utilizados, vamos explorar:

  • o papel dos pacotes na estrutura de um programa
  • como criar pacotes próprios
  • como reutilizar código entre diferentes partes de um projeto

Sem entrar ainda em detalhes de versionamento ou distribuição, o objetivo aqui é estabelecer uma base sólida para organização de código em Go, mantendo o mesmo fluxo prático adotado nos artigos anteriores.

Uso de pacotes

Até aqui, os programas utilizaram funcionalidades da biblioteca padrão por meio da palavra-chave import. Um exemplo recorrente é o uso do pacote fmt:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Nesse trecho, fmt.Println é uma função definida no pacote fmt. A forma de acesso segue um padrão simples:

nome_do_pacote.Nome

Esse modelo evita conflitos de nomes e torna explícita a origem de cada funcionalidade utilizada no código.

Importando múltiplos pacotes

Quando mais de um pacote é necessário, a declaração pode ser agrupada:

import (
    "fmt"
    "math"
)

O uso continua direto:

fmt.Println(math.Sqrt(16))

Papel do import

A declaração import não apenas torna funções disponíveis, mas também define dependências explícitas do programa.

Se um pacote é importado e não utilizado, o compilador gera erro. Esse comportamento força o código a permanecer limpo e evita dependências desnecessárias.

Observação

Neste ponto, o uso de pacotes ainda é direto: importar e utilizar.

Nos próximos tópicos, vamos sair desse cenário e começar a organizar código próprio em pacotes, aplicando o mesmo modelo já utilizado com a biblioteca padrão.

O que é um pacote

Em Go, um pacote é um conjunto de arquivos .go que pertencem ao mesmo diretório e são compilados juntos.

Todo arquivo começa com uma declaração package, que define a qual pacote aquele código pertence:

package main

Nos exemplos anteriores, utilizamos sempre o pacote main. Esse pacote tem um papel específico:

ele define um programa executável

Ou seja, apenas códigos dentro de package main podem ser executados diretamente.

Pacotes além do main

Nem todo código precisa ser executável. Na prática, a maior parte do código é organizada em pacotes reutilizáveis.

Por exemplo:

package calc

Esse código não será executado diretamente. Ele define funcionalidades que podem ser utilizadas por outros programas.

Organização por diretório

A relação entre pacote e diretório é direta:

calc/
├── sum.go
├── sub.go

Se ambos os arquivos começarem com:

package calc

eles pertencem ao mesmo pacote.

Subdiretórios

Subdiretórios são utilizados para organizar o projeto, e cada diretório define um pacote distinto do ponto de vista de importação.

Por exemplo:

calc/
├── sum.go
├── vector/
│   └── vector.go

Mesmo que os arquivos tenham a mesma declaração:

package calc

eles pertencem a pacotes diferentes:

  • calc
  • calc/vector

A distinção ocorre pelo caminho de importação, não apenas pelo nome do pacote.

Observação

Na prática, é comum manter o mesmo nome de pacote em diferentes diretórios quando esses diretórios fazem parte de um mesmo contexto lógico.

Isso permite organizar o código fisicamente sem necessariamente alterar a forma como ele é referenciado internamente.

No entanto, para o compilador, cada diretório continua sendo um pacote independente.

Regra prática (refinada)

  • um diretório → um pacote (unidade de compilação)
  • o nome do pacote pode se repetir em diferentes diretórios
  • o que diferencia pacotes é o caminho de importação

Criando um pacote próprio

Até aqui, utilizamos apenas pacotes da biblioteca padrão. O próximo passo é organizar código próprio da mesma forma.

Para isso, vamos construir um pacote simples chamado mathlib.

Estrutura do pacote

Crie um diretório para o pacote:

mathlib/
├── vector.go

Dentro de vector.go:

package mathlib

type Vector struct {
	X float64
	Y float64
	Z float64
}

func (v Vector) Add(u Vector) (r Vector) {
	r.X = v.X + u.X
	r.Y = v.Y + u.Y
	r.Z = v.Z + u.Z
	return
}

Esse arquivo define um pacote chamado mathlib com uma estrutura (Vector) e um método (Add).

Usando o pacote

Agora, em outro diretório, podemos utilizar esse código:

project/
├── mathlib/
│   └── vector.go
└── main.go

Conteúdo de main.go:

package main

import (
	"fmt"
	"project/mathlib"
)

func main() {
	a := mathlib.Vector{X: -1, Y: -1, Z: 0}
	b := mathlib.Vector{X: 3, Y: -2, Z: 1}

	fmt.Println("a:", a)
	fmt.Println("b:", b)
	fmt.Println("a+b:", a.Add(b))
}

Inicializando o módulo

Na raiz do projeto:

$ go mod init project

Execução

$ go run .
a: -1i + -1j
b: 3i + -2j + 1k
a+b: 2i + -3j + 1k

O que está acontecendo

  • o diretório mathlib define um pacote reutilizável
  • o programa principal importa esse pacote
  • estruturas e métodos são utilizados como qualquer pacote da biblioteca padrão

Observação

Note que nomes como Vector e Add começam com letra maiúscula.

Esse detalhe define o que pode ser acessado fora do pacote — e será explorado no próximo tópico.

Exportação

Em Go, a visibilidade de funções, tipos e variáveis é definida pela capitalização do nome.

Identificadores iniciados com letra maiúscula são exportados e podem ser acessados fora do pacote. Identificadores iniciados com letra minúscula são restritos ao pacote onde foram definidos.

Esse mecanismo define, na prática, o que o pacote expõe como API.

Evoluindo o mathlib

No exemplo anterior, o pacote mathlib continha apenas operações com vetores. Podemos estender esse pacote adicionando novas funcionalidades.

Um exemplo é a resolução de polinômios de segundo grau. Para isso, adicionamos um novo arquivo ao pacote:

mathlib/
├── vector.go
└── poly.go

Conteúdo de poly.go:

package mathlib

import (
	"fmt"
	"math"
)

func SolvePoly2(a, b, c float64) (x1, x2 float64, err error) {
	d := delta2(a, b, c)
	if d < 0 {
		return 0, 0, fmt.Errorf("there are no real roots")
	}

	rd := math.Sqrt(d)
	x1 = (-b + rd) / (2 * a)
	x2 = (-b - rd) / (2 * a)
	return
}

Agora o pacote mathlib passa a oferecer uma nova funcionalidade, sem alterar a estrutura existente.

Funções internas

A função SolvePoly2 utiliza um cálculo intermediário (delta). Esse cálculo não precisa ser exposto fora do pacote.

Podemos mantê-lo como uma função interna:

func delta2(a, b, c float64) float64 {
	return math.Pow(b, 2) - 4*a*c
}

Separação de responsabilidades

Nesse ponto, o pacote passa a ter dois níveis claros:

  • funções exportadas, que definem o comportamento do pacote
  • funções internas, que implementam esse comportamento

Do lado de fora do pacote, apenas SolvePoly2 está disponível:

x1, x2, err := mathlib.SolvePoly2(1, -3, 2)

A função delta2 não faz parte da interface pública e não pode ser utilizada diretamente.

Regra prática

  • exporte apenas o que representa comportamento do pacote
  • mantenha internas as funções auxiliares
  • evite expor detalhes de implementação

Organização do código

À medida que o pacote evolui, novas funcionalidades podem ser adicionadas sem alterar a forma como ele é utilizado.

No exemplo anterior, o pacote mathlib passou a conter mais de um arquivo:

mathlib/
├── vector.go
└── poly.go

Ambos os arquivos começam com:

package mathlib

Isso indica que pertencem ao mesmo pacote, mesmo estando separados fisicamente.

Divisão por responsabilidade

A separação em múltiplos arquivos não altera o comportamento do pacote, mas melhora a organização.

  • vector.go concentra operações com vetores
  • poly.go trata de polinômios

Essa divisão permite:

  • localizar código com mais facilidade
  • manter cada arquivo focado em um conjunto de responsabilidades
  • evoluir partes do pacote sem interferir diretamente nas demais

Unidade de compilação

Mesmo separados, os arquivos são compilados juntos.

Para o compilador, o pacote mathlib é uma única unidade.

Crescimento do pacote

À medida que o código cresce, essa organização passa a ser essencial.

Novas funcionalidades podem ser adicionadas criando novos arquivos dentro do mesmo pacote, mantendo o mesmo padrão:

mathlib/
├── vector.go
├── poly.go
└── matrix.go

Observação

A organização em arquivos não define a visibilidade dos elementos.

A visibilidade continua sendo controlada pela capitalização dos nomes, como visto anteriormente.

Conclusão

Neste artigo, os pacotes foram tratados como unidade central de organização em Go.

Partindo do uso de pacotes da biblioteca padrão, avançamos para a criação de código próprio, mantendo o mesmo modelo de uso: importar e utilizar. Ao longo do processo, alguns pontos se consolidam:

  • um pacote agrupa código relacionado em uma unidade coesa
  • a estrutura física (diretórios e arquivos) define essa organização
  • a API do pacote é definida explicitamente pela exportação
  • detalhes de implementação podem ser mantidos internos

Esse modelo permite evoluir o código de forma incremental, adicionando novas funcionalidades sem alterar o uso externo do pacote.

Na prática, isso reduz acoplamento e mantém a interface do pacote estável, mesmo quando sua implementação interna muda.

A partir deste ponto, o próximo passo natural é entender como esses pacotes são organizados em módulos e como o Go gerencia dependências entre eles.