本文 AI 產出,尚未審核
Golang 專案結構與最佳實踐
簡介
在 Go 語言的生態系統中,良好的專案結構不僅能提升程式碼的可讀性,也能讓團隊協作更順暢、部署更快速。許多新手在寫完第一個 main.go 後,往往直接把所有檔案堆在同一層目錄,久而久之會出現「檔案雜亂、相依關係難以追蹤」的問題。
本篇文章將說明 在實務專案中如何規劃目錄、切分套件、管理相依與測試,並提供多個可直接套用的程式碼範例,協助讀者從「零散」走向「可維護」的 Go 專案。
核心概念
1. 標準的目錄佈局
Go 官方文件(golang.org/doc/code.html)建議的目錄結構如下:
myapp/
├── cmd/ // 各種可執行檔的入口
│ └── myapp/ // main package
│ └── main.go
├── internal/ // 只在本模組內部使用的套件
│ └── foo/
│ └── foo.go
├── pkg/ // 供外部模組引用的套件
│ └── bar/
│ └── bar.go
├── api/ // OpenAPI / gRPC 定義
├── configs/ // 設定檔範例 (YAML, TOML …)
├── scripts/ // 部署、建置腳本
├── test/ // 整合測試或測試資料
├── go.mod
└── README.md
cmd/:每個子目錄對應一個可執行檔,裡面只放mainpackage。internal/:Go 1.4 起支援的特殊目錄,外部模組無法 import,適合放 私有 邏輯。pkg/:若想讓其他專案直接使用此套件,放在此目錄。api/、configs/、scripts/、test/:依需求自行擴充。
Tip:盡量避免在根目錄放太多
.go檔,保持根目錄僅有go.mod、README.md、main.go(若只有單一執行檔)等必要檔案。
2. 模組化與相依管理
使用 go.mod 建立模組後,所有相依都由 Go Modules 管理。以下是建立模組與加入相依的基本流程:
# 初始化模組,module 名稱建議使用 repository URL
go mod init github.com/yourname/myapp
# 加入第三方套件,例如 gin
go get github.com/gin-gonic/gin@v1.9.0
go.mod 會自動寫入:
module github.com/yourname/myapp
go 1.22
require (
github.com/gin-gonic/gin v1.9.0
)
最佳實踐:
- 固定版本(使用
@vX.Y.Z)避免因上游套件的非相容變更造成建置失敗。 - 定期執行
go mod tidy清理未使用的相依。
3. 套件切分的原則
- 單一職責:每個 package 只負責一件事,例如
pkg/db只處理資料庫連線與 CRUD。 - 低耦合:透過介面(interface)抽象依賴,讓測試與未來替換更簡單。
- 可測試:將副作用(I/O、網路)抽離到介面,讓核心邏輯保持純函式。
範例:抽象資料庫介面
// internal/repository/user_repo.go
package repository
import "github.com/yourname/myapp/internal/model"
// UserRepo 定義資料庫操作的抽象
type UserRepo interface {
Create(u *model.User) error
GetByID(id int64) (*model.User, error)
}
// internal/repository/mysql_user_repo.go
package repository
import (
"database/sql"
"github.com/yourname/myapp/internal/model"
)
type mysqlUserRepo struct {
db *sql.DB
}
func NewMySQLUserRepo(db *sql.DB) UserRepo {
return &mysqlUserRepo{db: db}
}
func (r *mysqlUserRepo) Create(u *model.User) error {
_, err := r.db.Exec("INSERT INTO users(name) VALUES(?)", u.Name)
return err
}
func (r *mysqlUserRepo) GetByID(id int64) (*model.User, error) {
row := r.db.QueryRow("SELECT id, name FROM users WHERE id=?", id)
var user model.User
if err := row.Scan(&user.ID, &user.Name); err != nil {
return nil, err
}
return &user, nil
}
在服務層只依賴 UserRepo 介面,測試時可以注入 mock:
// internal/service/user_service.go
package service
import "github.com/yourname/myapp/internal/repository"
type UserService struct {
repo repository.UserRepo
}
func NewUserService(r repository.UserRepo) *UserService {
return &UserService{repo: r}
}
// 其他業務方法…
4. 測試與 CI
- 單元測試:每個 package 內部放
*_test.go,使用testing套件。 - 表格測試(table‑driven test):讓測試案例易於擴充。
// internal/service/user_service_test.go
package service
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yourname/myapp/internal/model"
"github.com/yourname/myapp/internal/repository/mocks"
)
func TestCreateUser(t *testing.T) {
// 建立 mock repo
mockRepo := new(mocks.UserRepo)
mockRepo.On("Create", mock.Anything).Return(nil)
svc := NewUserService(mockRepo)
user := &model.User{Name: "Alice"}
err := svc.CreateUser(user)
assert.NoError(t, err)
mockRepo.AssertExpectations(t)
}
- CI(持續整合):使用 GitHub Actions、GitLab CI 或 Jenkins,設定
go test ./...、go vet、golint等檢查,確保每次提交都有自動測試。
5. 部署與容器化
在微服務環境下,最常見的部署方式是 Docker。以下是一個簡潔的 Dockerfile:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /myapp ./cmd/myapp
FROM alpine:latest
RUN addgroup -S app && adduser -S -G app app
USER app
COPY --from=builder /myapp /usr/local/bin/myapp
ENTRYPOINT ["myapp"]
- 多階段建置:減少最終映像檔大小。
- 非 root 使用者:提升安全性。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方式 |
|---|---|---|
| 把所有程式碼放在根目錄 | 隨著功能增長,go list ./... 會變慢,且難以定位檔案 |
依照上面的目錄佈局分層 |
直接在 main.go 中寫業務邏輯 |
測試困難、耦合度高 | 把業務抽到 service、repository 等套件 |
在 internal/ 之外使用私有套件 |
其他模組意外 import,破壞封裝 | 嚴格遵守 internal/ 規則 |
| 忽略介面抽象 | 無法 mock、測試受限 | 盡量以介面定義依賴,實作放在 internal/ |
未使用 go.mod tidy |
go.mod / go.sum 變得臃腫,CI 失敗 |
每次 pull request 前執行 go mod tidy |
最佳實踐小結
- 遵循官方建議的目錄結構,讓新加入的同事能快速上手。
- 使用介面抽象,讓業務邏輯與基礎設施解耦。
- 固定相依版本,避免因上游破壞性更新導致編譯失敗。
- 寫表格測試,保持測試可讀且易於擴充。
- Docker 多階段建置,確保映像檔最小化且安全。
實際應用場景
1. 建置一個簡易的 RESTful API
cmd/api/main.go只負責啟動 HTTP 伺服器。internal/handler/user_handler.go處理路由與請求參數驗證。internal/service/user_service.go實作業務邏輯。internal/repository/mysql_user_repo.go與 MySQL 互動。
透過依賴注入(DI)在 main.go 中組合:
package main
import (
"database/sql"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/yourname/myapp/internal/handler"
"github.com/yourname/myapp/internal/repository"
"github.com/yourname/myapp/internal/service"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 建立 DB 連線
db, err := sql.Open("mysql", "user:pwd@tcp(localhost:3306)/mydb")
if err != nil {
log.Fatalf("db connect error: %v", err)
}
defer db.Close()
// 注入相依
userRepo := repository.NewMySQLUserRepo(db)
userSvc := service.NewUserService(userRepo)
userHdl := handler.NewUserHandler(userSvc)
// 設定路由
r := gin.Default()
r.POST("/users", userHdl.Create)
r.GET("/users/:id", userHdl.GetByID)
// 啟動服務
if err := r.Run(":8080"); err != nil {
log.Fatalf("server error: %v", err)
}
}
2. 多服務微服務架構
- 每個微服務都有自己的
cmd/servicename/main.go。 - 共用的模型、錯誤定義放在
pkg/,讓其他服務可以直接 import。 - 透過
api/目錄管理 OpenAPI 3.0 或 gRPC.proto檔,保持介面一致性。
總結
良好的專案結構是維護大型 Go 應用的基石。本文從 目錄佈局、模組化、介面抽象、測試、部署 四大面向說明了實務上常見的做法與陷阱,並提供了 完整的程式碼範例,希望讀者在建立新專案或重構既有程式碼時,能夠快速套用這套最佳實踐。
只要遵循以下三個核心原則:
- 清晰的層級與職責分離(
cmd/、internal/、pkg/)。 - 介面驅動設計,讓相依注入與測試變得簡單。
- 持續的相依管理與自動化測試(
go.mod tidy、CI、Docker),
就能讓 Go 專案在 可讀性、可測試性、可部署性 三方面同時達到最佳狀態。祝開發順利,持續寫出乾淨、可維護的程式碼!