Một câu hỏi thường gặp của những người mới đến với Go là “Tôi nên tổ chức dự án Go của mình thế nào?”, xét về bố cục các tệp và thư mục. Mục tiêu của tài liệu này là đưa ra một số hướng dẫn giúp trả lời câu hỏi đó. Để tận dụng tối đa tài liệu này, hãy bảo đảm bạn đã nắm những kiến thức cơ bản về module Go bằng cách đọc hướng dẫn và quản lý mã nguồn module.
Dự án Go có thể bao gồm các package, các chương trình dòng lệnh, hoặc kết hợp cả hai. Hướng dẫn này được sắp xếp theo loại dự án.
Package cơ bản
Một package Go cơ bản có toàn bộ mã nằm trong thư mục gốc của dự án. Dự án gồm một module duy nhất, và module đó gồm một package duy nhất. Tên package trùng với thành phần đường dẫn cuối cùng của tên module. Với một package rất đơn giản chỉ cần một tệp Go, cấu trúc dự án là:
project-root-directory/
go.mod
modname.go
modname_test.go
[xuyên suốt tài liệu này, tên tệp/tên package hoàn toàn là tuỳ ý]
Giả sử thư mục này được tải lên một repository GitHub tại github.com/someuser/modname, dòng module trong tệp go.mod phải ghi là module github.com/someuser/modname.
Mã trong modname.go khai báo package bằng:
package modname
// ... package code here
Người dùng sau đó có thể dựa vào package này bằng cách import nó trong mã Go của họ với:
import "github.com/someuser/modname"
Một package Go có thể được chia thành nhiều tệp, tất cả nằm trong cùng một thư mục, ví dụ:
project-root-directory/
go.mod
modname.go
modname_test.go
auth.go
auth_test.go
hash.go
hash_test.go
Tất cả các tệp trong thư mục đều khai báo package modname.
Lệnh cơ bản
Một chương trình thực thi cơ bản (hay công cụ dòng lệnh) được cấu trúc tuỳ theo mức độ phức tạp và kích thước mã của nó. Chương trình đơn giản nhất có thể chỉ gồm một tệp Go duy nhất nơi func main được định nghĩa. Các chương trình lớn hơn có thể chia mã ra nhiều tệp, tất cả đều khai báo package main:
project-root-directory/
go.mod
auth.go
auth_test.go
client.go
main.go
Ở đây tệp main.go chứa func main, nhưng đó chỉ là quy ước. Tệp “main” cũng có thể được đặt tên modname.go (với một giá trị modname phù hợp) hoặc bất kỳ tên nào khác.
Giả sử thư mục này được tải lên một repository GitHub tại github.com/someuser/modname, dòng module trong tệp go.mod phải ghi là:
module github.com/someuser/modname
Và người dùng có thể cài đặt nó trên máy của họ bằng:
$ go install github.com/someuser/modname@latest
Package hoặc lệnh kèm các package hỗ trợ
Các package hoặc lệnh lớn hơn có thể hưởng lợi từ việc tách một phần chức năng ra thành các package hỗ trợ. Ban đầu, nên đặt các package như vậy vào một thư mục tên là internal; điều này ngăn các module khác phụ thuộc vào những package mà chúng tôi không nhất thiết muốn phơi bày và hỗ trợ cho việc dùng từ bên ngoài. Vì các dự án khác không thể import mã từ thư mục internal của chúng ta, chúng ta có thể tự do tái cấu trúc API của nó và nói chung là sắp xếp lại mọi thứ mà không làm hỏng người dùng bên ngoài. Do đó, cấu trúc dự án cho một package là:
project-root-directory/
internal/
auth/
auth.go
auth_test.go
hash/
hash.go
hash_test.go
go.mod
modname.go
modname_test.go
Tệp modname.go khai báo package modname, auth.go khai báo package auth và cứ thế. modname.go có thể import package auth như sau:
import "github.com/someuser/modname/internal/auth"
Bố cục cho một lệnh kèm các package hỗ trợ trong thư mục internal rất tương tự, chỉ khác là (các) tệp trong thư mục gốc khai báo package main.
Nhiều package
Một module có thể gồm nhiều package có thể import được; mỗi package có thư mục riêng, và có thể được tổ chức phân cấp. Đây là một cấu trúc dự án mẫu:
project-root-directory/
go.mod
modname.go
modname_test.go
auth/
auth.go
auth_test.go
token/
token.go
token_test.go
hash/
hash.go
internal/
trace/
trace.go
Xin nhắc lại, chúng ta giả định rằng dòng module trong go.mod ghi là:
module github.com/someuser/modname
Package modname nằm ở thư mục gốc, khai báo package modname và có thể được người dùng import bằng:
import "github.com/someuser/modname"
Các package con có thể được người dùng import như sau:
import "github.com/someuser/modname/auth"
import "github.com/someuser/modname/auth/token"
import "github.com/someuser/modname/hash"
Package trace nằm trong internal/trace không thể được import bên ngoài module này. Chúng tôi khuyến nghị giữ các package trong internal nhiều nhất có thể.
Nhiều lệnh
Nhiều chương trình trong cùng một repository thường sẽ có các thư mục riêng biệt:
project-root-directory/
go.mod
internal/
... shared internal packages
prog1/
main.go
prog2/
main.go
Trong mỗi thư mục, các tệp Go của chương trình khai báo package main. Một thư mục internal ở cấp cao nhất có thể chứa các package dùng chung cho tất cả các lệnh trong repository.
Người dùng có thể cài đặt những chương trình này như sau:
$ go install github.com/someuser/modname/prog1@latest
$ go install github.com/someuser/modname/prog2@latest
Một quy ước phổ biến là đặt tất cả các lệnh trong một repository vào thư mục cmd; dù điều này không thật sự cần thiết trong một repository chỉ gồm các lệnh, nó rất hữu ích trong một repository hỗn hợp có cả lệnh lẫn các package có thể import được, như chúng tôi sẽ bàn tiếp theo.
Package và lệnh trong cùng một repository
Đôi khi một repository sẽ cung cấp cả các package có thể import được lẫn các lệnh có thể cài đặt với chức năng liên quan tới nhau. Đây là một cấu trúc dự án mẫu cho repository như vậy:
project-root-directory/
go.mod
modname.go
modname_test.go
auth/
auth.go
auth_test.go
internal/
... internal packages
cmd/
prog1/
main.go
prog2/
main.go
Giả sử module này tên là github.com/someuser/modname, người dùng giờ đây vừa có thể import package từ nó:
import "github.com/someuser/modname"
import "github.com/someuser/modname/auth"
Vừa có thể cài đặt chương trình từ nó:
$ go install github.com/someuser/modname/cmd/prog1@latest
$ go install github.com/someuser/modname/cmd/prog2@latest
Dự án server
Go là một lựa chọn ngôn ngữ phổ biến để hiện thực server. Cấu trúc của những dự án như vậy rất khác nhau, do việc phát triển server có nhiều khía cạnh: giao thức (REST? gRPC?), triển khai, các tệp front-end, đóng gói container, script và nhiều thứ khác. Ở đây chúng tôi sẽ tập trung hướng dẫn vào những phần của dự án được viết bằng Go.
Các dự án server thường sẽ không có package để xuất ra ngoài, bởi một server thường là một binary khép kín (hoặc một nhóm binary). Vì vậy, chúng tôi khuyến nghị giữ các package Go hiện thực logic của server trong thư mục internal. Hơn nữa, vì dự án nhiều khả năng có nhiều thư mục khác chứa tệp không phải Go, giữ tất cả các lệnh Go cùng nhau trong thư mục cmd là một ý hay:
project-root-directory/
go.mod
internal/
auth/
...
metrics/
...
model/
...
cmd/
api-server/
main.go
metrics-analyzer/
main.go
...
... the project's other directories with non-Go code
Trong trường hợp repository của server phát sinh những package trở nên hữu ích để chia sẻ với các dự án khác, tốt nhất là tách chúng ra thành các module riêng.