本文 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/:每個子目錄對應一個可執行檔,裡面只放 main package。
  • internal/:Go 1.4 起支援的特殊目錄,外部模組無法 import,適合放 私有 邏輯。
  • pkg/:若想讓其他專案直接使用此套件,放在此目錄。
  • api/configs/scripts/test/:依需求自行擴充。

Tip:盡量避免在根目錄放太多 .go 檔,保持根目錄僅有 go.modREADME.mdmain.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. 套件切分的原則

  1. 單一職責:每個 package 只負責一件事,例如 pkg/db 只處理資料庫連線與 CRUD。
  2. 低耦合:透過介面(interface)抽象依賴,讓測試與未來替換更簡單。
  3. 可測試:將副作用(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 vetgolint 等檢查,確保每次提交都有自動測試。

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 中寫業務邏輯 測試困難、耦合度高 把業務抽到 servicerepository 等套件
internal/ 之外使用私有套件 其他模組意外 import,破壞封裝 嚴格遵守 internal/ 規則
忽略介面抽象 無法 mock、測試受限 盡量以介面定義依賴,實作放在 internal/
未使用 go.mod tidy go.mod / go.sum 變得臃腫,CI 失敗 每次 pull request 前執行 go mod tidy

最佳實踐小結

  1. 遵循官方建議的目錄結構,讓新加入的同事能快速上手。
  2. 使用介面抽象,讓業務邏輯與基礎設施解耦。
  3. 固定相依版本,避免因上游破壞性更新導致編譯失敗。
  4. 寫表格測試,保持測試可讀且易於擴充。
  5. 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 應用的基石。本文從 目錄佈局、模組化、介面抽象、測試、部署 四大面向說明了實務上常見的做法與陷阱,並提供了 完整的程式碼範例,希望讀者在建立新專案或重構既有程式碼時,能夠快速套用這套最佳實踐。

只要遵循以下三個核心原則:

  1. 清晰的層級與職責分離cmd/internal/pkg/)。
  2. 介面驅動設計,讓相依注入與測試變得簡單。
  3. 持續的相依管理與自動化測試go.mod tidy、CI、Docker),

就能讓 Go 專案在 可讀性、可測試性、可部署性 三方面同時達到最佳狀態。祝開發順利,持續寫出乾淨、可維護的程式碼!