Voltar ao blog

A palavra-chave import em Go: sintaxe, aliases, imports em branco e com ponto

Como funciona a palavra-chave import do Go: imports agrupados, caminhos de importação, porque é que um import não usado faz falhar a build, aliases, imports em branco e com ponto, e como resolver ciclos de importação.

A palavra-chave import em Go: sintaxe, aliases, imports em branco e com ponto

A palavra-chave import torna disponíveis num ficheiro Go os nomes exportados de outro pacote. Os imports vêm logo a seguir à cláusula package e antes de qualquer outra declaração, e cada um identifica um pacote pelo seu caminho de importação, como "net/http" ou "github.com/jackc/pgx/v5". Depois, acedes ao conteúdo desse pacote através do nome, como em http.ListenAndServe. Um import pode também levar um alias, o identificador em branco _ ou um ponto ., e cada forma muda a maneira como o nome do pacote fica associado ao ficheiro (especificação do Go).

Resumo rápido

  • Os imports vêm depois de package e antes de tudo o resto. A maioria dos ficheiros usa um único bloco agrupado import ( ... ).
  • Um caminho de importação é uma string. Os caminhos da biblioteca padrão são curtos ("encoding/json"), e todos os outros começam com um caminho de módulo vindo de go.mod.
  • Um import não usado é um erro de compilação: "os" imported and not used. O goimports e o gopls acrescentam e removem imports por ti.
  • alias "path" dá outro nome a um pacote dentro de um ficheiro. Usa-o quando dois imports têm o mesmo nome, como html/template e text/template.
  • _ "path" importa um pacote só pelos seus efeitos secundários, que correm nas suas funções init. Os drivers de base de dados e o net/http/pprof funcionam assim.
  • . "path" traz os nomes para o ficheiro sem qualificador. Os guias de estilo desaconselham-no e o staticcheck assinala-o como ST1001, tirando alguns casos raros em testes.
  • O Go recusa ciclos de importação. A solução é um novo pacote partilhado ou uma interface definida onde é usada.
  • Os imports têm âmbito de ficheiro. Cada ficheiro de um pacote importa o que usa, mesmo quando um ficheiro vizinho já importa o mesmo pacote.

Como se importa um pacote em Go?

Escreve import seguido do caminho do pacote entre aspas. Um ficheiro que precisa de um só pacote pode usar uma única linha, e um ficheiro que precisa de vários usa um bloco agrupado entre parênteses:

example.gogo
package main

import "fmt"

func main() {
	fmt.Println("ok")
}
example.gogo
package main

import (
	"encoding/json"
	"log"
	"net/http"
)

type healthResponse struct {
	Status string `json:"status"`
}

func main() {
	http.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(healthResponse{Status: "ok"})
	})
	log.Fatal(http.ListenAndServe(":8080", nil))
}

As duas formas significam o mesmo, e quase todo o código Go usa o bloco agrupado. Os imports vêm sempre depois da cláusula package e antes de qualquer const, var, type ou func. Se puseres um mais abaixo, o parser para com syntax error: imports must appear before other declarations. Também não há forma de importar dentro de uma função.

As ferramentas de formatação ordenam o bloco por ti. O gofmt ordena as linhas de cada grupo pelo caminho de importação. O goimports, que a maioria dos editores executa ao guardar, divide também o bloco em dois grupos separados por uma linha em branco, com a biblioteca padrão primeiro e tudo o resto a seguir:

example.gogo
import (
	"context"
	"fmt"
	"net/http"

	"example.com/shop/billing"
	"github.com/jackc/pgx/v5"
)

A ordem não tem efeito nenhum no programa. Mantém os diffs pequenos e deixa quem lê ver logo que dependências vêm de fora da biblioteca padrão.

O que é um caminho de importação em Go?

Um caminho de importação é a string que diz ao comando go onde vive um pacote. Os pacotes da biblioteca padrão têm caminhos curtos, sem ponto no primeiro elemento, como "fmt", "net/http" ou "crypto/rand". O caminho de qualquer outro pacote começa com um caminho de módulo.

Os teus próprios pacotes usam o caminho do módulo declarado em go.mod mais o diretório. Com module example.com/shop em go.mod, o pacote em ./billing importa-se como "example.com/shop/billing". No modo de módulos não há imports relativos como "./billing", por isso um pacote local importa-se sempre pelo caminho completo.

Os pacotes de terceiros funcionam da mesma maneira, e o comando go resolve-os através do go.mod:

  1. O go get github.com/jackc/pgx/v5 descarrega o módulo e acrescenta uma linha require ao go.mod.
  2. Escreves "github.com/jackc/pgx/v5" no bloco de imports.
  3. Mais tarde, o go mod tidy acrescenta os módulos de que os teus imports precisam e remove os que já nada importa.

O último elemento do caminho é normalmente o nome do pacote, mas nem sempre. "github.com/jackc/pgx/v5" fornece um pacote chamado pgx, e "gopkg.in/yaml.v3" fornece yaml. A palavra-chave package em Go explica como os nomes e os caminhos se relacionam.

Porque é que o Go se recusa a compilar imports não usados?

O Go trata um import não usado como um erro, não como um aviso. Deixa "os" num ficheiro que já não o chama e o go build para:

example.texttext
./main.go:5:2: "os" imported and not used

A FAQ do Go explica a razão. Um import não usado torna a compilação mais lenta e acrescenta uma dependência de que o programa não precisa, e numa base de código grande estas acumulam-se. As pessoas ignoram os avisos, por isso o Go trata isto como um erro. O go mod tidy faz a mesma limpeza no go.mod, removendo os requisitos que nada importa.

Na prática, raramente os corriges à mão. O goimports e o gopls, o servidor de linguagem do Go por trás do suporte a Go no VS Code e no GoLand, apagam os imports não usados e acrescentam os que faltam quando guardas. Se estiveres a fazer debugging e quiseres manter um import por um minuto, atribui algo dele ao identificador em branco, como em var _ = os.Exit, e remove a linha antes de fazer commit.

Como se dá outro nome a um import em Go?

Põe um alias antes do caminho. O alias substitui o nome do pacote, mas só nesse ficheiro:

example.gogo
import htmltemplate "html/template"

A razão habitual é um conflito de nomes. O html/template e o text/template chamam-se ambos template, e um serviço de email precisa muitas vezes dos dois: texto simples para o assunto e saída com escape de HTML para o corpo. Importar os dois sem alias falha com template redeclared in this block. Basta um alias num deles:

example.gogo
package main

import (
	htmltemplate "html/template"
	"os"
	"text/template"
)

func main() {
	subject := template.Must(template.New("subject").Parse("Your invoice {{.ID}}\n"))
	body := htmltemplate.Must(htmltemplate.New("body").Parse("<p>{{.Note}}</p>\n"))

	data := map[string]string{"ID": "INV-42", "Note": "<script>alert(1)</script>"}
	subject.Execute(os.Stdout, data)
	body.Execute(os.Stdout, data)
}

O resultado mostra porque é que os dois pacotes existem em separado. O assunto sai como Your invoice INV-42, e o corpo sai como <p>&lt;script&gt;alert(1)&lt;/script&gt;</p>, com o escape feito pelo html/template.

Os aliases aparecem também quando muitos pacotes partilham um último elemento genérico. O código do Kubernetes importa vários pacotes que se chamam todos v1, por isso escreve corev1 "k8s.io/api/core/v1" e metav1 "k8s.io/apimachinery/pkg/apis/meta/v1". Quando não há conflito, mantém o nome verdadeiro. Toda a gente reconhece http., mas um alias inventado obriga quem lê a voltar ao bloco de imports para ver o que significa.

O que faz um import em branco _ em Go?

Um import em branco carrega um pacote sem associar o seu nome. Não podes chamar nada dele, mas o pacote é inicializado na mesma, por isso as variáveis ao nível do pacote recebem valor e as funções init correm. Alguns pacotes fazem o trabalho útil no init, registando-se noutro pacote.

O exemplo mais comum é um driver de database/sql:

example.gogo
package main

import (
	"database/sql"
	"log"
	"os"

	_ "github.com/jackc/pgx/v5/stdlib"
)

func main() {
	db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
}

O pacote stdlib do pgx chama sql.Register("pgx", ...) no seu init. O teu código só fala com o database/sql, mas sem o import em branco o driver nunca se regista e o sql.Open devolve sql: unknown driver "pgx" (forgotten import?).

Outros imports em branco que vais encontrar em serviços reais:

ImportO que faz o seu init
_ "github.com/jackc/pgx/v5/stdlib"Regista o driver pgx no database/sql
_ "net/http/pprof"Acrescenta handlers de profiling /debug/pprof/ ao http.DefaultServeMux
_ "image/png"Regista o descodificador de PNG para que o image.Decode consiga ler ficheiros PNG
_ "time/tzdata"Embute a base de dados de fusos horários, para containers que não a têm
_ "embed"Nada em tempo de execução. É obrigatório para o //go:embed funcionar numa variável string ou []byte

O caso do embed é uma regra da toolchain e não um efeito secundário de um init. Se deixares o import de fora, a build falha com go:embed requires import "embed" (or import _ "embed", if package is not used).

A ordem de inicialização é previsível. Cada pacote importado fica totalmente inicializado, incluindo os seus próprios imports, antes do pacote que o importa. Desde o Go 1.21, a especificação fixa também a ordem entre pacotes sem relação entre si, que são inicializados pela ordem dos caminhos de importação. Dentro de um pacote, as variáveis ao nível do pacote recebem valor primeiro, e depois as funções init correm pela ordem em que os ficheiros são apresentados ao compilador, que o comando go ordena pelo nome do ficheiro. O main corre em último. Um pacote importado por vários outros é inicializado uma única vez.

Põe os imports em branco no main ou no pacote que precisa realmente do efeito secundário, não numa biblioteca que outras pessoas importam. Uma biblioteca que faz um import em branco de net/http/pprof expõe endpoints de profiling em todos os programas que a usam, quer o programa os queira, quer não.

Deves usar imports com ponto em Go?

Quase nunca. Um import com ponto junta os nomes exportados de um pacote ao ficheiro, por isso chamas esses nomes sem o nome do pacote:

example.gogo
import . "strings"

func normalizeEmail(email string) string {
	return ToLower(TrimSpace(email))
}

Numa função de três linhas, isto percebe-se bem. Num ficheiro longo, quem vê ToLower não consegue saber se está definida neste pacote nem de que import vem. A página Go Code Review Comments desaconselha-o, e o staticcheck assinala-o:

example.texttext
main.go:3:8: should not use dot imports (ST1001)

O único uso aceite é um teste que tem de viver fora do pacote que testa por causa de um ciclo. O Code Review Comments dá o exemplo de um package foo_test que importa bar/testutil, que por sua vez importa foo. Um import com ponto de foo permite que esse teste se leia como se estivesse dentro do pacote. Algumas frameworks de testes, como o Ginkgo e o Gomega, também documentam imports com ponto para os seus matchers. Fora desses casos, escreve o nome do pacote.

O que é um ciclo de importação em Go?

Um ciclo de importação são dois ou mais pacotes que se importam uns aos outros, diretamente ou através de uma cadeia. O Go proíbe-os, por isso billing a importar customers enquanto customers importa billing falha na build:

example.texttext
package example.com/shop/billing
	imports example.com/shop/customers from billing.go
	imports example.com/shop/billing from customers.go: import cycle not allowed

A regra mantém a ordem de inicialização bem definida e as builds rápidas, porque o compilador consegue sempre compilar um pacote depois de tudo aquilo de que ele depende. Também diz algo sobre o design, porque um ciclo significa normalmente que dois pacotes são na verdade um só, ou que ambos dependem de algo que pertence a um terceiro.

Há duas soluções habituais:

  1. Move a parte partilhada para um pacote próprio. Se billing e customers precisam ambos de um tipo CustomerID, põe-no num pequeno pacote shop/ids ou shop/domain que não importe nenhum dos dois.
  2. Define uma interface onde ela é usada. Se o billing só precisa de obter o email de um cliente, pode declarar aquilo de que precisa e deixar o main passar a implementação real:
example.gogo
package billing

import "context"

// CustomerLookup is the one thing billing needs from the customers package.
type CustomerLookup interface {
	Email(ctx context.Context, customerID string) (string, error)
}

type Service struct {
	customers CustomerLookup
}

func NewService(customers CustomerLookup) *Service {
	return &Service{customers: customers}
}

Agora o billing já não importa customers, e o customers pode importar billing à vontade. Qualquer tipo com um método Email compatível satisfaz a interface sem a nomear, por isso a dependência aponta num só sentido.

O comando go aplica uma regra parecida ao package main. Nenhum pacote o pode importar, e se tentares recebes import "example.com/shop/cmd/api" is a program, not an importable package.

Os imports em Go têm âmbito de ficheiro ou de pacote?

Os imports têm âmbito de ficheiro. Um import só associa o nome do pacote no ficheiro que o contém, embora todos os outros nomes ao nível do pacote sejam partilhados por todos os ficheiros do pacote. Aqui, o server.go importa log:

example.gogo
package main

import "log"

func main() {
	log.Println("server starting")
	runJobs()
}

E o jobs.go, no mesmo pacote, chama log sem o importar:

example.gogo
package main

func runJobs() {
	log.Println("running jobs")
}

O runJobs é visível em server.go porque as funções ao nível do pacote são partilhadas. O import de log não é, por isso a build falha:

example.texttext
./jobs.go:4:2: undefined: log

Cada ficheiro lista os seus próprios imports, e o goimports acrescenta-os por ti. Isto também significa que o nome de um import pode entrar em conflito com uma declaração ao nível do pacote noutro ficheiro. Declara var log = ... num ficheiro enquanto outro ficheiro importa log e recebes log already declared through import of package log ("log").

Os aliases também têm âmbito de ficheiro. Dar o nome mrand "math/rand/v2" num ficheiro não tem efeito nenhum nos outros ficheiros do pacote.

O Go também restringe os pacotes que podes importar. Um pacote dentro de um diretório internal/ só pode ser importado por código cuja raiz seja o diretório pai de internal. A palavra-chave package em Go cobre o internal/ e a forma como a visibilidade funciona entre pacotes.

Onde entra o LevelUpGo

O LevelUpGo ensina Go através de exercícios que executam código Go real no navegador. O curso Packages & Organization cobre nomes exportados, init e estado do pacote, módulos Go e dependências, e os comandos de módulos que usas todos os dias, como go get e go mod tidy. Se estás a começar, o Go Basics vem primeiro. Para as outras 24 palavras reservadas, vê Palavras-chave do Go: as 25 explicadas.

Perguntas frequentes

O import é uma palavra-chave em Go?

Sim. O import é uma das 25 palavras-chave reservadas do Go, por isso não o podes usar como nome de variável, de função ou de pacote. Só pode aparecer em declarações de import no topo de um ficheiro.

Posso importar um pacote dentro de uma função em Go?

Não. Os imports só são permitidos ao nível de topo de um ficheiro, logo a seguir à cláusula package. Um import dentro do corpo de uma função falha com syntax error: unexpected keyword import. Se só precisas de um pacote numa função, importa-o na mesma no topo do ficheiro.

Como se importa um pacote local em Go?

Usa o caminho do módulo em go.mod seguido do diretório do pacote. Num módulo chamado example.com/shop, o pacote em ./internal/pricing importa-se como "example.com/shop/internal/pricing". Os módulos Go não suportam imports relativos como "./pricing".

Qual é a diferença entre import _ e import . em Go?

O import _ "path" executa a inicialização do pacote mas não te dá forma nenhuma de o referir. Serve para efeitos secundários, como registar um driver de base de dados. O import . "path" faz o contrário e põe todos os nomes exportados do pacote diretamente no teu ficheiro, para os chamares sem qualificador. Os imports em branco são comuns em Go idiomático, enquanto os imports com ponto são desaconselhados.

A ordem dos imports importa em Go?

Não. O compilador não quer saber da ordem das linhas num bloco de imports, e a ordem de inicialização é determinada pelo grafo de dependências, não pela forma como listas os imports. O gofmt ordena-os pelo caminho e o goimports separa a biblioteca padrão dos outros pacotes, mas isso serve só para facilitar a leitura.

Fontes

Escreve Go como um engenheiro sénior

Lições interativas no navegador. As primeiras são grátis.

Experimenta uma lição grátisOu cria uma conta gratuita