Hướng dẫn về Bazel: Tạo một dự án Go

Báo cáo vấn đề Xem nguồn Nightly · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Hướng dẫn này giới thiệu cho bạn những kiến thức cơ bản về Bazel bằng cách hướng dẫn bạn cách tạo một dự án Go (Golang). Bạn sẽ tìm hiểu cách thiết lập không gian làm việc, tạo một chương trình nhỏ, nhập một thư viện và chạy thử nghiệm. Trong quá trình này, bạn sẽ tìm hiểu các khái niệm chính về Bazel, chẳng hạn như mục tiêu và tệp BUILD.

Thời gian hoàn thành ước tính: 30 phút

Trước khi bắt đầu

Cài đặt Bazel

Trước khi bắt đầu, trước tiên hãy cài đặt bazel nếu bạn chưa cài đặt.

Bạn có thể kiểm tra xem Bazel đã được cài đặt hay chưa bằng cách chạy bazel version trong bất kỳ thư mục nào.

Cài đặt Go (không bắt buộc)

Bạn không cần cài đặt Go để tạo các dự án Go bằng Bazel. Nhóm quy tắc Bazel Go sẽ tự động tải xuống và sử dụng một chuỗi công cụ Go thay vì sử dụng chuỗi công cụ được cài đặt trên máy của bạn. Điều này đảm bảo tất cả nhà phát triển trong một dự án đều xây dựng bằng cùng một phiên bản Go.

Tuy nhiên, bạn vẫn có thể muốn cài đặt một chuỗi công cụ Go để chạy các lệnh như go getgo mod tidy.

Bạn có thể kiểm tra xem Go đã được cài đặt hay chưa bằng cách chạy go version trong bất kỳ thư mục nào.

Tải dự án mẫu xuống

Các ví dụ về Bazel được lưu trữ trong một kho lưu trữ Git, vì vậy, bạn cần cài đặt Git nếu chưa cài đặt. Để tải kho lưu trữ ví dụ xuống, hãy chạy lệnh sau:

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

Dự án mẫu cho hướng dẫn này nằm trong thư mục examples/go-tutorial. Xem nội dung của tệp:

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

Có 3 thư mục con (stage1, stage2stage3), mỗi thư mục dành cho một phần khác nhau trong hướng dẫn này. Mỗi giai đoạn đều dựa trên giai đoạn trước đó.

Tạo bằng Bazel

Bắt đầu trong thư mục stage1, nơi chúng ta sẽ tìm thấy một chương trình. Chúng ta có thể tạo ứng dụng bằng bazel build, sau đó chạy ứng dụng:

$ 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! 💚

Chúng ta cũng có thể tạo và chạy chương trình bằng một lệnh bazel run duy nhất:

$ 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! 💚

Tìm hiểu cấu trúc dự án

Hãy xem dự án mà chúng ta vừa tạo.

hello.go chứa mã nguồn Go cho chương trình.

package main

import "fmt"

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

BUILD chứa một số hướng dẫn cho Bazel, cho biết những gì chúng ta muốn tạo. Thông thường, bạn sẽ viết một tệp như thế này trong mỗi thư mục. Đối với dự án này, chúng ta có một mục tiêu go_binary duy nhất để tạo chương trình từ hello.go.

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

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

MODULE.bazel theo dõi các phần phụ thuộc của dự án. Thao tác này cũng đánh dấu thư mục gốc của dự án, vì vậy, bạn sẽ chỉ ghi một tệp MODULE.bazel cho mỗi dự án. Tệp này có mục đích tương tự như tệp go.mod của Go. Bạn không thực sự cần tệp go.mod trong dự án Bazel, nhưng bạn vẫn nên có một tệp để có thể tiếp tục sử dụng go getgo mod tidy cho việc quản lý phần phụ thuộc. Bộ quy tắc Bazel Go có thể nhập các phần phụ thuộc từ go.mod, nhưng chúng ta sẽ đề cập đến vấn đề này trong một hướng dẫn khác.

Tệp MODULE.bazel của chúng tôi chứa một phần phụ thuộc duy nhất vào rules_go, bộ quy tắc Go. Chúng ta cần phần phụ thuộc này vì Bazel không có tính năng hỗ trợ tích hợp cho Go.

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

Cuối cùng, MODULE.bazel.lock là một tệp do Bazel tạo ra, chứa các hàm băm và siêu dữ liệu khác về các phần phụ thuộc của chúng ta. Nó bao gồm các phần phụ thuộc ngầm do chính Bazel thêm vào, vì vậy, nó khá dài và chúng ta sẽ không thấy nó ở đây. Giống như go.sum, bạn nên cam kết tệp MODULE.bazel.lock với tính năng kiểm soát nguồn để đảm bảo mọi người trong dự án của bạn đều nhận được cùng một phiên bản của từng phần phụ thuộc. Bạn không cần phải chỉnh sửa MODULE.bazel.lock theo cách thủ công.

Tìm hiểu về tệp BUILD

Hầu hết hoạt động tương tác của bạn với Bazel sẽ thông qua các tệp BUILD (hoặc tương đương, các tệp BUILD.bazel), vì vậy, điều quan trọng là bạn phải hiểu được chức năng của các tệp này.

Tệp BUILD được viết bằng một ngôn ngữ kịch bản có tên là Starlark, một tập hợp con có giới hạn của Python.

Tệp BUILD chứa danh sách mục tiêu. Mục tiêu là thứ mà Bazel có thể tạo, chẳng hạn như một tệp nhị phân, thư viện hoặc kiểm thử.

Một mục tiêu gọi một hàm quy tắc bằng danh sách thuộc tính để mô tả những gì cần được tạo. Ví dụ của chúng ta có 2 thuộc tính: name xác định mục tiêu trên dòng lệnh và srcs là danh sách đường dẫn tệp nguồn (phân tách bằng dấu gạch chéo, tương ứng với thư mục chứa tệp BUILD).

Quy tắc cho Bazel biết cách tạo một mục tiêu. Trong ví dụ của mình, chúng tôi đã sử dụng quy tắc go_binary. Mỗi quy tắc xác định các hành động (lệnh) tạo ra một nhóm tệp đầu ra. Ví dụ: go_binary xác định các thao tác biên dịch và liên kết Go để tạo ra một tệp đầu ra có thể thực thi.

Bazel có các quy tắc tích hợp cho một số ngôn ngữ như Java và C++. Bạn có thể xem tài liệu của các quy tắc này trong Bách khoa toàn thư về bản dựng. Bạn có thể tìm thấy các bộ quy tắc cho nhiều ngôn ngữ và công cụ khác trên Sổ đăng ký trung tâm Bazel (BCR).

Thêm thư viện

Chuyển sang thư mục stage2, nơi chúng ta sẽ tạo một chương trình mới in ra vận mệnh của bạn. Chương trình này sử dụng một gói Go riêng biệt làm thư viện để chọn một câu nói hay trong danh sách các thông báo được xác định trước.

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

fortune.go là tệp nguồn của thư viện. Thư viện fortune là một gói Go riêng biệt, vì vậy, các tệp nguồn của thư viện này nằm trong một thư mục riêng. Bazel không yêu cầu bạn giữ các gói Go trong các thư mục riêng biệt, nhưng đây là một quy ước mạnh mẽ trong hệ sinh thái Go và việc tuân theo quy ước này sẽ giúp bạn duy trì khả năng tương thích với các công cụ Go khác.

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))]
}

Thư mục fortune có tệp BUILD riêng cho Bazel biết cách tạo gói này. Ở đây, chúng ta sử dụng go_library thay vì go_binary.

Chúng ta cũng cần đặt thuộc tính importpath thành một chuỗi mà thư viện có thể được nhập vào các tệp nguồn Go khác. Tên này phải là đường dẫn kho lưu trữ (hoặc đường dẫn mô-đun) được nối với thư mục trong kho lưu trữ.

Cuối cùng, chúng ta cần đặt thuộc tính visibility thành ["//visibility:public"]. Bạn có thể đặt visibility trên mọi mục tiêu. Nó xác định những gói Bazel có thể phụ thuộc vào mục tiêu này. Trong trường hợp này, chúng ta muốn mọi mục tiêu đều có thể phụ thuộc vào thư viện này, vì vậy, chúng ta sử dụng giá trị đặc biệt //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"],
)

Bạn có thể tạo thư viện này bằng cách:

$ bazel build //fortune

Tiếp theo, hãy xem cách print_fortune.go sử dụng gói này.

package main

import (
    "fmt"

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

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

print_fortune.go nhập gói bằng cách sử dụng cùng một chuỗi được khai báo trong thuộc tính importpath của thư viện fortune.

Chúng ta cũng cần khai báo phần phụ thuộc này cho Bazel. Dưới đây là tệp BUILD trong thư mục stage2.

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

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

Bạn có thể chạy lệnh này bằng lệnh bên dưới.

bazel run //:print_fortune

Mục tiêu print_fortune có một thuộc tính deps, một danh sách các mục tiêu khác mà mục tiêu này phụ thuộc vào. Tệp này chứa "//fortune", một chuỗi nhãn đề cập đến mục tiêu trong thư mục fortune có tên là fortune.

Bazel yêu cầu tất cả các mục tiêu khai báo rõ ràng các phần phụ thuộc của chúng bằng các thuộc tính như deps. Điều này có vẻ rườm rà vì các phần phụ thuộc cũng được chỉ định trong tệp nguồn, nhưng tính rõ ràng của Bazel mang lại cho nó một lợi thế. Bazel tạo một biểu đồ hành động chứa tất cả các lệnh, đầu vào và đầu ra trước khi chạy bất kỳ lệnh nào, mà không cần đọc bất kỳ tệp nguồn nào. Sau đó, Bazel có thể lưu kết quả của hành động vào bộ nhớ đệm hoặc gửi các hành động để thực thi từ xa mà không cần logic dành riêng cho ngôn ngữ được tích hợp sẵn.

Hiểu nhãn

Nhãn là một chuỗi mà Bazel dùng để xác định mục tiêu hoặc tệp. Nhãn được dùng trong các đối số dòng lệnh và trong các thuộc tính tệp BUILD như deps. Chúng ta đã thấy một vài ví dụ, chẳng hạn như //fortune, //:print-fortune@rules_go//go:def.bzl.

Nhãn có 3 phần: tên kho lưu trữ, tên gói và tên mục tiêu (hoặc tên tệp).

Tên kho lưu trữ được viết giữa @//, đồng thời được dùng để tham chiếu đến một mục tiêu từ một mô-đun Bazel khác (vì các lý do trước đây, mô-đunkho lưu trữ đôi khi được dùng thay thế cho nhau). Trong nhãn @rules_go//go:def.bzl, tên kho lưu trữ là rules_go. Bạn có thể bỏ qua tên kho lưu trữ khi đề cập đến các mục tiêu trong cùng một kho lưu trữ.

Tên gói được viết giữa //:, đồng thời được dùng để tham chiếu đến một mục tiêu trong một gói Bazel khác. Trong nhãn @rules_go//go:def.bzl, tên gói là go. Gói Bazel là một tập hợp các tệp và mục tiêu được xác định bằng tệp BUILD hoặc BUILD.bazel trong thư mục cấp cao nhất. Tên gói của tệp này là một đường dẫn được phân tách bằng dấu gạch chéo từ thư mục gốc của mô-đun (chứa MODULE.bazel) đến thư mục chứa tệp BUILD. Một gói có thể bao gồm các thư mục con, nhưng chỉ khi các thư mục con đó không chứa các tệp BUILD xác định gói riêng của chúng.

Hầu hết các dự án Go đều có một tệp BUILD cho mỗi thư mục và một gói Go cho mỗi tệp BUILD. Bạn có thể bỏ qua tên gói trong nhãn khi đề cập đến các mục tiêu trong cùng một thư mục.

Tên mục tiêu được viết sau : và đề cập đến một mục tiêu trong một gói. Bạn có thể bỏ qua tên mục tiêu nếu tên đó giống với thành phần cuối cùng của tên gói (do đó, //a/b/c:c giống với //a/b/c; //fortune:fortune giống với //fortune).

Trên dòng lệnh, bạn có thể dùng ... làm ký tự đại diện để tham chiếu đến tất cả các mục tiêu trong một gói. Điều này hữu ích cho việc tạo hoặc kiểm thử tất cả các mục tiêu trong một kho lưu trữ.

# Build everything
$ bazel build //...

Kiểm thử dự án

Tiếp theo, hãy chuyển đến thư mục stage3, nơi chúng ta sẽ thêm một bài kiểm thử.

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

fortune/fortune_test.go là tệp nguồn kiểm thử mới của chúng ta.

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)
    }
}

Tệp này dùng biến fortunes chưa xuất, nên cần được biên dịch vào cùng một gói Go như fortune.go. Hãy xem tệp BUILD để biết cách hoạt động:

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"],
)

Chúng tôi có một mục tiêu fortune_test mới sử dụng quy tắc go_test để biên dịch và liên kết một tệp thực thi kiểm thử. go_test cần biên dịch fortune.gofortune_test.go cùng với một lệnh, vì vậy, chúng ta sử dụng thuộc tính embed ở đây để kết hợp các thuộc tính của mục tiêu fortune vào fortune_test. embed thường được dùng nhất với go_testgo_binary, nhưng cũng hoạt động với go_library. Đôi khi, điều này hữu ích cho mã đã tạo.

Bạn có thể thắc mắc liệu thuộc tính embed có liên quan đến gói embed của Go hay không. Gói này được dùng để truy cập vào các tệp dữ liệu được sao chép vào một tệp thực thi. Đây là một trường hợp trùng tên không mong muốn: thuộc tính embed của rules_go được giới thiệu trước gói embed của Go. Thay vào đó, rules_go sử dụng embedsrcs để liệt kê các tệp có thể được tải bằng gói embed.

Hãy thử chạy kiểm thử bằng 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.

Bạn có thể sử dụng ký tự đại diện ... để chạy tất cả các kiểm thử. Bazel cũng sẽ tạo các mục tiêu không phải là kiểm thử, vì vậy, điều này có thể phát hiện lỗi biên dịch ngay cả trong những gói không có kiểm thử.

$ bazel test //...

Kết luận và tài liệu đọc thêm

Trong hướng dẫn này, chúng ta đã tạo và kiểm thử một dự án Go nhỏ bằng Bazel, đồng thời tìm hiểu một số khái niệm cốt lõi về Bazel trong quá trình này.

  • Để bắt đầu tạo các ứng dụng khác bằng Bazel, hãy xem hướng dẫn cho C++, Java, AndroidiOS.
  • Bạn cũng có thể xem danh sách các quy tắc được đề xuất cho các ngôn ngữ khác.
  • Để biết thêm thông tin về Go, hãy xem mô-đun rules_go, đặc biệt là tài liệu các quy tắc Core Go.
  • Để tìm hiểu thêm về cách làm việc với các mô-đun Bazel bên ngoài dự án, hãy xem phần các phần phụ thuộc bên ngoài. Cụ thể, để biết thông tin về cách phụ thuộc vào các mô-đun và chuỗi công cụ Go thông qua hệ thống mô-đun của Bazel, hãy xem phần Go với bzlmod.