Tutorial do Bazel: criar um projeto Go

Informar um problema Ver código-fonte Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Este tutorial apresenta os conceitos básicos do Bazel mostrando como criar um projeto Go (Golang). Você aprenderá a configurar seu espaço de trabalho, criar um pequeno programa, importar uma biblioteca e executar o teste dela. Ao longo do caminho, você aprenderá os principais conceitos do Bazel, como destinos e arquivos BUILD.

Tempo estimado de conclusão: 30 minutos

Antes de começar

Instalar o Bazel

Antes de começar, instale o Bazel, caso ainda não tenha feito.

Para verificar se o Bazel está instalado, execute bazel version em qualquer diretório.

Instalar o Go (opcional)

Você não precisa instalar o Go para criar projetos Go com o Bazel. O conjunto de regras do Bazel Go faz o download automático e usa uma cadeia de ferramentas Go em vez da instalada na sua máquina. Isso garante que todos os desenvolvedores em um projeto sejam criados com a mesma versão do Go.

No entanto, talvez você ainda queira instalar uma cadeia de ferramentas do Go para executar comandos como go get e go mod tidy.

Para verificar se o Go está instalado, execute go version em qualquer diretório.

Acessar o projeto de amostra

Os exemplos do Bazel são armazenados em um repositório Git. Portanto, você precisará instalar o Git, caso ainda não tenha feito isso. Para fazer o download do repositório de exemplos, execute este comando:

git clone https://github.com/bazelbuild/examples

O projeto de exemplo deste tutorial está no diretório examples/go-tutorial. Confira o que ele contém:

go-tutorial/
└── stage1
└── stage2
└── stage3

Há três subdiretórios (stage1, stage2 e stage3), cada um para uma seção diferente deste tutorial. Cada etapa é baseada na anterior.

Criação com o Bazel

Comece no diretório stage1, onde vamos encontrar um programa. Podemos criá-lo com bazel build e, em seguida, executá-lo:

$ cd go-tutorial/stage1/
$ bazel build //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-bin/hello_/hello
INFO: Elapsed time: 0.473s, Critical Path: 0.25s
INFO: 3 processes: 1 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions

$ bazel-bin/hello_/hello
Hello, Bazel! 💚

Também podemos criar a execução do programa com um único comando bazel run:

$ bazel run //:hello
bazel run //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-bin/hello_/hello
INFO: Elapsed time: 0.128s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/hello_/hello
Hello, Bazel! 💚

Noções básicas sobre a estrutura do projeto

Confira o projeto que acabamos de criar.

hello.go contém o código-fonte Go do programa.

package main

import "fmt"

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

BUILD contém algumas instruções para o Bazel, informando o que queremos criar. Normalmente, você grava um arquivo como este em cada diretório. Para este projeto, temos um único destino go_binary que cria nosso programa a partir de hello.go.

load("@rules_go//go:def.bzl", "go_binary")

go_binary(
    name = "hello",
    srcs = ["hello.go"],
)

O MODULE.bazel rastreia as dependências do projeto. Ele também marca o diretório raiz do projeto. Portanto, grave apenas um arquivo MODULE.bazel por projeto. Ele serve a uma finalidade semelhante ao arquivo go.mod do Go. Você não precisa de um arquivo go.mod em um projeto do Bazel, mas pode ser útil ter um para continuar usando go get e go mod tidy para gerenciamento de dependências. O conjunto de regras do Bazel Go pode importar dependências de go.mod, mas vamos abordar isso em outro tutorial.

Nosso arquivo MODULE.bazel contém uma única dependência em rules_go, o conjunto de regras do Go. Precisamos dessa dependência porque o Bazel não tem suporte integrado para Go.

bazel_dep(
    name = "rules_go",
    version = "0.50.1",
)

Por fim, MODULE.bazel.lock é um arquivo gerado pelo Bazel que contém hashes e outros metadados sobre nossas dependências. Ele inclui dependências implícitas adicionadas pelo próprio Bazel, então é muito longo e não vamos mostrar aqui. Assim como go.sum, confirme o arquivo MODULE.bazel.lock no controle de origem para garantir que todos no projeto recebam a mesma versão de cada dependência. Não é necessário editar MODULE.bazel.lock manualmente.

Entender o arquivo BUILD

A maior parte da sua interação com o Bazel será por meio de arquivos BUILD (ou, equivalentemente, arquivos BUILD.bazel). Por isso, é importante entender o que eles fazem.

Os arquivos BUILD são escritos em uma linguagem de script chamada Starlark, um subconjunto limitado do Python.

Um arquivo BUILD contém uma lista de alvos. Um destino é algo que o Bazel pode criar, como um binário, uma biblioteca ou um teste.

Um destino chama uma função de regra com uma lista de atributos para descrever o que precisa ser criado. Nosso exemplo tem dois atributos: name identifica o destino na linha de comando, e srcs é uma lista de caminhos de arquivos de origem (separados por barra, relativos ao diretório que contém o arquivo BUILD).

Uma regra diz ao Bazel como criar um destino. No nosso exemplo, usamos a regra go_binary. Cada regra define ações (comandos) que geram um conjunto de arquivos de saída. Por exemplo, go_binary define ações de compilação e vinculação do Go que produzem um arquivo de saída executável.

O Bazel tem regras integradas para algumas linguagens, como Java e C++. A documentação está disponível na Encyclopedia do Build. Você pode encontrar conjuntos de regras para muitos outros idiomas e ferramentas no Registro Central do Bazel (BCR).

Adicionar uma biblioteca

Vá para o diretório stage2, onde vamos criar um novo programa que imprime sua fortuna. Esse programa usa um pacote Go separado como uma biblioteca que seleciona uma previsão de uma lista predefinida de mensagens.

go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│   ├── BUILD
│   └── fortune.go
└── print_fortune.go

fortune.go é o arquivo de origem da biblioteca. A biblioteca fortune é um pacote Go separado. Portanto, os arquivos de origem estão em um diretório separado. O Bazel não exige que você mantenha pacotes Go em diretórios separados, mas é uma convenção forte no ecossistema Go, e seguir essa regra vai ajudar você a manter compatibilidade com outras ferramentas Go.

package fortune

import "math/rand"

var fortunes = []string{
    "Your build will complete quickly.",
    "Your dependencies will be free of bugs.",
    "Your tests will pass.",
}

func Get() string {
    return fortunes[rand.Intn(len(fortunes))]
}

O diretório fortune tem o próprio arquivo BUILD, que informa ao Bazel como criar esse pacote. Usamos go_library aqui em vez de go_binary.

Também precisamos definir o atributo importpath como uma string com a qual a biblioteca pode ser importada para outros arquivos de origem do Go. Esse nome precisa ser o caminho do repositório (ou caminho do módulo) concatenado com o diretório dentro do repositório.

Por fim, precisamos definir o atributo visibility como ["//visibility:public"]. visibility pode ser definido em qualquer destino. Ele determina quais pacotes do Bazel podem depender desse destino. No nosso caso, queremos que qualquer destino possa depender dessa biblioteca. Por isso, usamos o valor especial //visibility:public.

load("@rules_go//go:def.bzl", "go_library")

go_library(
    name = "fortune",
    srcs = ["fortune.go"],
    importpath = "github.com/bazelbuild/examples/go-tutorial/stage2/fortune",
    visibility = ["//visibility:public"],
)

É possível criar essa biblioteca com:

$ bazel build //fortune

Em seguida, confira como print_fortune.go usa esse pacote.

package main

import (
    "fmt"

    "github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)

func main() {
    fmt.Println(fortune.Get())
}

print_fortune.go importa o pacote usando a mesma string declarada no atributo importpath da biblioteca fortune.

Também precisamos declarar essa dependência para o Bazel. Este é o arquivo BUILD no diretório stage2.

load("@rules_go//go:def.bzl", "go_binary")

go_binary(
    name = "print_fortune",
    srcs = ["print_fortune.go"],
    deps = ["//fortune"],
)

Para isso, use o comando abaixo.

bazel run //:print_fortune

O destino print_fortune tem um atributo deps, uma lista de outros destinos dos quais ele depende. Ele contém "//fortune", uma string de rótulo que se refere ao destino no diretório fortune chamado fortune.

O Bazel exige que todos os destinos declarem as dependências explicitamente com atributos como deps. Isso pode parecer complicado, já que as dependências também são especificadas nos arquivos de origem, mas a explicabilidade do Bazel oferece uma vantagem. O Bazel cria um gráfico de ações que contém todos os comandos, entradas e saídas antes de executar qualquer comando, sem ler nenhum arquivo de origem. O Bazel pode armazenar em cache os resultados de ação ou enviar ações para execução remota sem a lógica integrada específica da linguagem.

Como compreender os rótulos

Um rótulo é uma string que o Bazel usa para identificar um destino ou um arquivo. Os rótulos são usados em argumentos de linha de comando e em atributos de arquivo BUILD, como deps. Já vimos alguns, como //fortune, //:print-fortune e @rules_go//go:def.bzl.

Um rótulo tem três partes: um nome de repositório, um nome de pacote e um nome de destino (ou arquivo).

O nome do repositório é escrito entre @ e // e é usado para se referir a um destino de um módulo Bazel diferente. Por motivos históricos, módulo e repositório às vezes são usados como sinônimos. No rótulo, @rules_go//go:def.bzl, o nome do repositório é rules_go. O nome do repositório pode ser omitido ao se referir a destinos no mesmo repositório.

O nome do pacote é escrito entre // e : e é usado para se referir a um destino de um pacote Bazel diferente. No rótulo @rules_go//go:def.bzl, o nome do pacote é go. Um pacote do Bazel é um conjunto de arquivos e alvos definidos por um arquivo BUILD ou BUILD.bazel no diretório de nível superior. O nome do pacote é um caminho separado por barra do diretório raiz do módulo (que contém MODULE.bazel) para o diretório que contém o arquivo BUILD. Um pacote pode incluir subdiretórios, mas somente se eles não contiverem arquivos BUILD que definam os próprios pacotes.

A maioria dos projetos Go tem um arquivo BUILD por diretório e um pacote Go por arquivo BUILD. O nome do pacote em um rótulo pode ser omitido ao se referir a destinos no mesmo diretório.

O nome do destino é escrito após : e se refere a um destino dentro de um pacote. O nome do destino pode ser omitido se for igual ao último componente do nome do pacote. Por exemplo, //a/b/c:c é igual a //a/b/c, //fortune:fortune é igual a //fortune.

Na linha de comando, é possível usar ... como um curinga para se referir a todos os destinos em um pacote. Isso é útil para criar ou testar todos os destinos em um repositório.

# Build everything
$ bazel build //...

Testar seu projeto

Em seguida, vá para o diretório stage3, onde vamos adicionar um teste.

go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│   ├── BUILD
│   ├── fortune.go
│   └── fortune_test.go
└── print-fortune.go

fortune/fortune_test.go é o novo arquivo de origem de teste.

package fortune

import (
    "slices"
    "testing"
)

// TestGet checks that Get returns one of the strings from fortunes.
func TestGet(t *testing.T) {
    msg := Get()
    if i := slices.Index(fortunes, msg); i < 0 {
        t.Errorf("Get returned %q, not one the expected messages", msg)
    }
}

Esse arquivo usa a variável fortunes não exportada, então ele precisa ser compilado no mesmo pacote Go que fortune.go. Confira o arquivo BUILD para saber como isso funciona:

load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
    name = "fortune",
    srcs = ["fortune.go"],
    importpath = "github.com/bazelbuild/examples/go-tutorial/stage3/fortune",
    visibility = ["//visibility:public"],
)

go_test(
    name = "fortune_test",
    srcs = ["fortune_test.go"],
    embed = [":fortune"],
)

Temos um novo destino fortune_test que usa a regra go_test para compilar e vincular um executável de teste. O go_test precisa compilar fortune.go e fortune_test.go com o mesmo comando. Portanto, usamos o atributo embed aqui para incorporar os atributos do destino fortune em fortune_test. embed é mais comumente usado com go_test e go_binary, mas também funciona com go_library, que às vezes é útil para o código gerado.

Talvez você esteja se perguntando se o atributo embed está relacionado ao pacote embed do Go, que é usado para acessar arquivos de dados copiados para um executável. Há uma colisão de nome: o atributo embed de rules_go foi introduzido antes do pacote embed do Go. Em vez disso, rules_go usa o embedsrcs para listar arquivos que podem ser carregados com o pacote embed.

Tente executar nosso teste com bazel test:

$ bazel test //fortune:fortune_test
INFO: Analyzed target //fortune:fortune_test (0 packages loaded, 0 targets configured).
INFO: Found 1 test target...
Target //fortune:fortune_test up-to-date:
  bazel-bin/fortune/fortune_test_/fortune_test
INFO: Elapsed time: 0.168s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
//fortune:fortune_test                                          PASSED in 0.3s

Executed 0 out of 1 test: 1 test passes.
There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.

É possível usar o curinga ... para executar todos os testes. Ele também cria destinos que não são testes, o que pode detectar erros de compilação mesmo em pacotes que não têm testes.

$ bazel test //...

Conclusão e leitura complementar

Neste tutorial, criamos e testamos um pequeno projeto Go com o Bazel e aprendemos alguns conceitos principais do Bazel.

  • Para começar a criar outros aplicativos com o Bazel, consulte os tutoriais para C++, Java, Android e iOS.
  • Você também pode conferir a lista de regras recomendadas para outros idiomas.
  • Para mais informações sobre o Go, consulte o módulo rules_go, em especial a documentação Regras principais do Go.
  • Para saber mais sobre como trabalhar com módulos do Bazel fora do seu projeto, consulte dependências externas. Para saber como depender de módulos e toolchains do Go pelo sistema de módulo do Bazel, consulte Go com bzlmod.