Bắt đầu với một tệp
Một chương trình Go có thể nằm trọn trong một tệp main.go, và các công cụ nhỏ nên như vậy. Chương trình hoàn chỉnh dưới đây đọc đầu vào, xử lý rồi in ra báo cáo, tất cả trong một package:
Khi chương trình lớn dần, hãy tách nó thành nhiều tệp trong cùng thư mục và cùng package (parse.go, report.go, main.go). Các tệp trong một package dùng chung mọi định danh, nên không cần export hay import gì. Thường thì đó là tất cả cấu trúc mà một dự án vài nghìn dòng cần.
Nhớ rằng một package main gồm nhiều tệp phải được chạy như một package: go run ., không phải go run main.go. Nếu không, mỗi tệp được biên dịch riêng lẻ và bạn sẽ gặp lỗi undefined với những tên được định nghĩa ở các tệp khác.
Không có bố cục bắt buộc
Go không yêu cầu cấu trúc thư mục nào ngoài quy tắc "mỗi thư mục là một package". Hướng dẫn chính thức là trang Organizing a Go module trên go.dev, và trang này mô tả vài dạng bố cục, không phải quy tắc.
Repository GitHub golang-standards/project-layout được sao chép rất nhiều và cũng bị nhầm là một chuẩn rất nhiều. Đó là bộ sưu tập quy ước của cộng đồng rút ra từ các dự án lớn, và Russ Cox, khi đó là trưởng nhóm kỹ thuật của Go, đã mở một issue ở đó nói rõ rằng nó không phải chuẩn của Go. Phần lớn thư mục trong đó (pkg/, api/, build/, deployments/) chỉ có ý nghĩa với codebase lớn. Sao chép nguyên bộ khung vào một dự án mới chỉ tạo ra các thư mục rỗng và đường dẫn import dài mà chẳng được lợi gì.
Các quy tắc mà công cụ thực sự áp dụng rất ngắn:
- Một thư mục là một package. Mọi tệp
.gotrong đó (trừ các tệp_test.godùng hậu tố_test) có chung một tên package. - Đường dẫn import là đường dẫn module cộng với đường dẫn thư mục.
- Thư mục tên
internalgiới hạn ai được import những gì bên trong nó. - Các thư mục tên
testdata, và những thư mục bắt đầu bằng.hoặc_, bị công cụ go bỏ qua.
Các dạng bố cục phổ biến
Một lệnh duy nhất
todo/
├── go.mod module github.com/you/todo
├── main.go
├── parse.go
├── report.go
└── parse_test.go
Phẳng, mọi thứ trong package main. go install github.com/you/todo@latest chạy được và tệp thực thi có tên todo.
Một thư viện
slug/
├── go.mod module github.com/you/slug
├── slug.go package slug
├── slug_test.go
├── example_test.go
└── internal/
└── table/ helpers the public package uses, hidden from users
Package nằm ở gốc module, nên người dùng import github.com/you/slug và gọi slug.Make(...). Bất cứ thứ gì bạn không muốn hỗ trợ như API public thì đặt dưới internal/.
Một service hoặc nhiều tệp thực thi
shop/
├── go.mod module github.com/you/shop
├── cmd/
│ ├── shop-api/
│ │ └── main.go package main: flags, config, wiring
│ └── shop-worker/
│ └── main.go
├── internal/
│ ├── order/ package order: domain types and logic
│ │ ├── order.go
│ │ └── order_test.go
│ ├── store/ package store: database access
│ └── httpapi/ package httpapi: handlers, routing
├── migrations/
└── README.md
cmd/<name>/main.go chứa mỗi tệp thực thi một thư mục, và tên thư mục trở thành tên tệp thực thi (go build ./cmd/shop-api tạo ra shop-api). Mỗi main giữ thật mỏng: đọc cấu hình, tạo các phụ thuộc, khởi động máy chủ. Code thật nằm trong các package dưới internal/, nơi cả hai tệp thực thi dùng được còn module khác thì không.
Đây là bố cục mà phần lớn service Go cuối cùng đều đi tới. Hãy dùng nó khi bạn có tệp thực thi thứ hai hoặc có lý do thật sự để tách package, đừng dùng ngay từ ngày đầu.
internal/ được lệnh go kiểm soát
Một package dưới internal/ chỉ có thể được import bởi code nằm trong cây thư mục có gốc là thư mục cha của internal. Từ một module khác, quá trình build sẽ thất bại:
package example.com/b
main.go:6:2: use of internal package example.com/a/internal/secret not allowed
Điều đó biến internal/ thành công cụ để giữ phạm vi API thật nhỏ. Code ở đó có thể thay đổi thoải mái, vì bạn biết mọi nơi gọi đều nằm trong repository của mình. Với một ứng dụng, đặt gần như mọi thứ vào internal/ là hợp lý. Với một thư viện, nó tách những gì bạn cam kết giữ ổn định khỏi những gì không cam kết.
internal hoạt động ở mọi độ sâu: shop/internal/order nhìn thấy được từ toàn bộ shop/, còn shop/internal/order/internal/pricing chỉ nhìn thấy được bên trong shop/internal/order/.
Đặt tên package
Tên package xuất hiện ở mọi chỗ gọi, nên nó quan trọng hơn cả cây thư mục.
- Ngắn, viết thường, một từ:
order,store,httpapi. Không dấu gạch dưới, không mixedCaps. - Đặt theo thứ nó cung cấp, không theo thứ nó chứa.
util,common,helpers,miscvàmodelskhông nói lên điều gì và trở thành bãi chứa đủ thứ; hướng dẫn phong cách của đội ngũ Go khuyên tránh những tên này một cách rõ ràng. Hãy đặt hàm hỗ trợ trong package dùng nó, hoặc trong một package được đặt tên theo mục đích (slug,retry). - Tránh lặp từ. Nơi gọi viết
order.Ordervàorder.New, nên đừng đặt tênorder.OrderServicehayorder.NewOrder. Thư viện chuẩn làm điều này nhất quán:http.Server, không phảihttp.HTTPServer. - Tên thư mục và tên package nên trùng nhau, trừ
package maintrong các thư mục lệnh. Khi chúng khác nhau, người đọc phải mở tệp ra mới biết import đó đưa vào định danh nào.
Hãy tách package theo trách nhiệm, không theo loại. Cách chia models/, controllers/, services/ (phổ biến ở các hệ sinh thái khác) buộc mỗi tính năng phải đụng tới ba package và dễ tạo ra import vòng tròn, điều Go cấm. Các package được tổ chức quanh một khái niệm nghiệp vụ (order, payment, user) giữ kiểu dữ liệu và logic của chúng ở cùng một chỗ.
Đặt test ở đâu
Test nằm cạnh code trong cùng thư mục, trong các tệp tên *_test.go. Không có cây thư mục tests/.
package ordertrongorder_test.go: test nội bộ, dùng được các định danh unexported.package order_testtrong cùng thư mục: test bên ngoài, chỉ nhìn thấy API exported như một nơi gọi bình thường. Hữu ích cho các ví dụ và để tránh import vòng tròn trong test.- Tệp dữ liệu mẫu nằm trong thư mục
testdata/cạnh các test. Công cụ go bỏ qua thư mục này, và test chạy với thư mục package làm thư mục làm việc, nên các đường dẫn tương đối nhưtestdata/big.jsondùng được.
Các tệp khác ở thư mục gốc
| Tệp hoặc thư mục | Mục đích |
|---|---|
go.mod, go.sum | định nghĩa module và checksum của các phụ thuộc, luôn ở thư mục gốc |
README.md, LICENSE | như mọi dự án khác |
Makefile hoặc Taskfile.yml | lối tắt build, không bắt buộc |
Dockerfile | thường ở thư mục gốc với các service |
migrations/, web/, docs/ | tài nguyên không phải Go, đặt tên theo thứ chúng chứa |
tools.go | cách cũ để cố định phiên bản công cụ; Go 1.24 thay nó bằng các dòng tool trong go.mod (go get -tool) |
go.work | workspace để phát triển nhiều module cùng lúc trên máy; thường không commit với dự án chỉ có một module |
Lỗi thường gặp
- Sao chép một template lớn cho dự án nhỏ. Hãy bắt đầu phẳng và thêm thư mục khi xuất hiện tệp thực thi thứ hai hoặc một ranh giới thật sự.
- Package tên
utilshaycommon. Hãy đặt tên package theo việc nó làm. - Mỗi tệp hay mỗi kiểu một package. Package trong Go được thiết kế là đơn vị lớn hơn class trong Java. Một package có mười tệp là bình thường.
- Import vòng tròn. Hai package không thể import lẫn nhau. Điều này thường có nghĩa là chúng thuộc về nhau, rằng một kiểu dùng chung nên chuyển xuống package ở tầng thấp hơn, hoặc rằng một bên nên phụ thuộc vào một interface nhỏ thay vì package kia.
- Dùng
pkg/theo phản xạ. Nó thêm một đoạn vào mọi đường dẫn import mà không thêm ý nghĩa. - Đặt test trong thư mục riêng. Ở đó chúng không nhìn thấy code unexported và công cụ cũng không tìm chúng ở đó.
Câu hỏi thường gặp
Go có bố cục dự án chính thức không?
Không có bố cục bắt buộc. Đội ngũ Go công bố hướng dẫn tại go.dev/doc/modules/layout, mô tả vài dạng phổ biến (một package, một lệnh, nhiều lệnh với internal/). Repository nổi tiếng golang-standards/project-layout là dự án cộng đồng, không phải chuẩn của Go, và đội ngũ Go đã nói rõ điều này công khai.
Thư mục internal trong Go là gì?
Một package có đường dẫn import chứa phần tử internal chỉ có thể được import bởi code nằm dưới thư mục cha của thư mục internal đó. example.com/app/internal/store import được ở bất kỳ đâu dưới example.com/app/, còn lệnh go từ chối import từ mọi module khác với lỗi use of internal package ... not allowed.
Có nên dùng thư mục pkg trong Go không?
Không bắt buộc, và nó chỉ thêm một đoạn vào đường dẫn mà không thêm ý nghĩa. Một số dự án lớn dùng pkg/ để tách code thư viện public khỏi phần còn lại, nhưng thư viện chuẩn của Go và phần lớn dự án hiện đại thì không. Hãy đặt các package import được ở gốc module hoặc trong các thư mục có tên rõ ràng, còn mọi thứ riêng tư thì để trong internal/.
Tệp test đặt ở đâu trong dự án Go?
Trong cùng thư mục với code mà chúng kiểm thử, đặt tên *_test.go. Go không có cây thư mục test riêng. Các tệp dữ liệu mẫu (fixture) nằm trong thư mục testdata cạnh các test, và công cụ go bỏ qua thư mục này khi tìm package.