Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Go Module 與 Package 完整筆記

go.mod 檔案長什麼樣

module myapp          // 模組名稱(自行命名)

go 1.21               // Go 版本

require (
  github.com/gin-gonic/gin v1.9.0  // 相依套件
)

常用指令

指令用途備註
go mod init <名稱>建立新模組一個專案通常只用一次
go mod tidy整理相依套件移除沒用到的、補上缺少的
go get <套件>新增或更新套件例:go get github.com/gin-gonic/gin
go mod download下載所有相依到本機快取CI 環境常用
go mod vendor把相依複製進 vendor/ 資料夾離線環境或公司內網常用

go.mod vs go.sum

檔案內容要手動改嗎?
go.mod模組名稱、Go 版本、相依清單可以,但通常交給指令處理
go.sum套件的雜湊校驗值不要手動改

package 是什麼

Go 的程式碼以 package(套件) 為單位組織,每個 .go 檔案第一行都要宣告自己屬於哪個 package。

package main // 可執行程式的入口,一定叫 main
package utils // 自訂 package,名稱自行決定

modulepackage 的關係

module myapp               ← 整個專案的名稱(go.mod 定義)
├── package main           ← main.go
├── package utils          ← utils/helper.go
└── package models         ← models/user.go
  • module = 整個專案或函式庫(一個 go.mod 對應一個 module)
  • package = module 裡的子資料夾,用來分類程式碼

import 怎麼寫

import (
  "fmt"                      // 標準函式庫
  "myapp/utils"              // 自己的 package(模組名 + 路徑)
  "github.com/gin-gonic/gin" // 外部套件
)

專案結構範例

myapp/                        ← module: myapp
├── go.mod
├── go.sum
├── main.go                   ← package main
├── utils/
│   └── helper.go             ← package utils
├── models/
│   └── user.go               ← package models
└── handlers/
    └── api.go                ← package handlers

package 命名規則

規則說明
小寫英文package utils ✅,package Utils
和資料夾同名handlers/api.go 對應 package handlers
大寫 = 公開func Hello() 可被其他 package 使用
小寫 = 私有func hello() 只有同 package 內可以用

容易誤解的地方

1. 模組名稱不一定要 github.com/...

  • 自己用:go mod init myapp 就好
  • 要發布讓別人 go get:才需要對應真實 URL
go mod init github.com/你的帳號/專案名

2. go mod 不只是用在 GitHub

任何 Go 專案都建議使用 go mod,因為它負責管理相依套件版本,和 GitHub 沒有直接關係。

3. 單一 .go 檔可以不用 go mod

go run main.go # 沒有外部套件時,直接跑即可

但只要用到外部套件,或專案中有多個 package,就建議使用 go mod

4. go.sum 會自動產生,不要手動編輯

執行 go mod tidygo get 後,go.sum 會自動更新。

5. 資料夾名稱不一定等於 package 名稱

技術上可以不同,但強烈建議保持一致,否則 import 時很容易搞混。

6. package main 只能有一個 main() 函式入口

整個 module 裡可以有很多個 package main,例如 cmd/ 下放多個工具;但每個可執行程式都只能有一個 main() 入口。

myapp/
├── cmd/
│   ├── server/main.go   ← package main(啟動伺服器)
│   └── cli/main.go      ← package main(命令列工具)
└── internal/
    └── db/db.go         ← package db(共用邏輯)

初始化步驟

mkdir myapp
cd myapp
go mod init myapp       # 建立 go.mod
# 開始寫 main.go...
go get github.com/xxx   # 新增外部套件
go mod tidy             # 整理相依
go build ./...          # 編譯全部
go run main.go          # 執行

實際操作範例(完整流程)

範例一:多 Package 專案從零開始

目標:建立一個有 utilsmodels 兩個 package 的小工具。

# Step 1:建立目錄並初始化
mkdir -p myapp/utils myapp/models
cd myapp
go mod init myapp

go.mod 初始內容(自動生成):

module myapp

go 1.21

models/user.go

package models

import "fmt"

type User struct {
    ID   int
    Name string
    Age  int
}

// String 讓 fmt.Println 直接輸出好看格式
func (u User) String() string {
    return fmt.Sprintf("User{ID:%d, Name:%s, Age:%d}", u.ID, u.Name, u.Age)
}

// IsAdult 是公開方法(大寫開頭)
func (u User) IsAdult() bool {
    return u.Age >= 18
}

// validate 是私有方法(小寫開頭)
func (u User) validate() bool {
    return u.Name != "" && u.Age > 0
}

utils/helper.go

package utils

import "strings"

// FilterAdults 只保留成年人(公開函式)
func FilterAdults(users []string) []string {
    var result []string
    for _, name := range users {
        name = strings.TrimSpace(name)
        if name != "" {
            result = append(result, name)
        }
    }
    return result
}

// Greet 回傳問候語
func Greet(name string) string {
    return "Hello, " + name + "!"
}

main.go

package main

import (
    "fmt"
    "myapp/models"  // 模組名 + 資料夾路徑
    "myapp/utils"
)

func main() {
    // 使用 models package
    u := models.User{ID: 1, Name: "Alice", Age: 25}
    fmt.Println(u)             // User{ID:1, Name:Alice, Age:25}
    fmt.Println(u.IsAdult())   // true

    // 使用 utils package
    names := []string{"Bob", "Charlie", "  "}
    filtered := utils.FilterAdults(names)
    fmt.Println(filtered)       // [Bob Charlie]

    fmt.Println(utils.Greet("World"))  // Hello, World!
}

執行結果

$ go run main.go
User{ID:1, Name:Alice, Age:25}
true
[Bob Charlie]
Hello, World!

範例二:新增外部套件(以 uuid 為例)

# 安裝外部套件
go get github.com/google/uuid

go.mod 變化(自動更新):

module myapp

go 1.21

require github.com/google/uuid v1.6.0

使用範例

package main

import (
    "fmt"
    "github.com/google/uuid"
)

func main() {
    id := uuid.New()
    fmt.Println(id)           // e.g., 550e8400-e29b-41d4-a716-446655440000
    fmt.Println(id.String())  // 同上,字串形式
}
$ go run main.go
3f6a6b3d-1c2e-4e5f-a7b8-9d0e1f2a3b4c

範例三:指定版本、升降版

# 安裝特定版本
go get github.com/google/uuid@v1.3.0

# 升級到最新版
go get github.com/google/uuid@latest

# 升級到特定版本
go get github.com/google/uuid@v1.6.0

# 移除套件(先從程式碼移除 import,再執行)
go mod tidy

查看目前所有相依版本

$ go list -m all
myapp
github.com/google/uuid v1.6.0

範例四:replace 指令(本地開發替換)

當你正在同時開發兩個 module(例如開發一個函式庫並在另一個專案測試它),可以用 replace 讓 Go 直接讀本機版本,不從網路抓。

目錄結構

~/projects/
├── mylib/        ← 你正在開發的函式庫
│   ├── go.mod    (module github.com/you/mylib)
│   └── lib.go
└── myapp/        ← 使用該函式庫的專案
    ├── go.mod
    └── main.go

mylib/go.mod

module github.com/you/mylib

go 1.21

mylib/lib.go

package mylib

func Hello() string {
    return "Hello from local mylib!"
}

myapp/go.mod(加入 replace):

module myapp

go 1.21

require github.com/you/mylib v0.0.0

replace github.com/you/mylib => ../mylib   ← 指向本機路徑

myapp/main.go

package main

import (
    "fmt"
    "github.com/you/mylib"
)

func main() {
    fmt.Println(mylib.Hello())  // Hello from local mylib!
}
$ cd myapp && go run main.go
Hello from local mylib!

注意replace 只影響當前 module,不會傳遞給依賴你的人。要發布前記得移除 replace。


範例五:internal package(受保護的內部套件)

internal/ 資料夾內的 package 只有同一個 module 可以 import,外部套件無法引用。

myapp/
├── go.mod                    (module myapp)
├── main.go
├── internal/
│   └── auth/
│       └── token.go          ← package auth(受保護)
└── api/
    └── handler.go            ← 可以 import internal/auth

internal/auth/token.go

package auth

import "crypto/rand"
import "encoding/hex"

// GenerateToken 產生隨機 token(只限內部使用)
func GenerateToken() string {
    b := make([]byte, 16)
    rand.Read(b)
    return hex.EncodeToString(b)
}

api/handler.go(同一 module,可以 import):

package api

import (
    "fmt"
    "myapp/internal/auth"  // ✅ 同 module,可以用
)

func HandleLogin() {
    token := auth.GenerateToken()
    fmt.Println("Token:", token)
}

外部 module 嘗試 import 會報錯

# otherpkg/main.go
import "myapp/internal/auth"
# Error: use of internal package myapp/internal/auth not allowed

範例六:多個可執行檔(cmd/ 模式)

myapp/
├── go.mod
├── cmd/
│   ├── server/
│   │   └── main.go    ← 啟動 HTTP server
│   └── migrate/
│       └── main.go    ← 執行資料庫遷移
└── internal/
    └── db/
        └── db.go

cmd/server/main.go

package main

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Println("Starting server on :8080")
    http.ListenAndServe(":8080", nil)
}

cmd/migrate/main.go

package main

import "fmt"

func main() {
    fmt.Println("Running database migration...")
}

分別編譯與執行

# 執行特定 cmd
go run ./cmd/server
go run ./cmd/migrate

# 編譯全部
go build ./cmd/...

# 個別編譯
go build -o bin/server ./cmd/server
go build -o bin/migrate ./cmd/migrate

範例七:go work(多 Module 工作區,Go 1.18+)

當你在同一個機器上同時修改多個相互依賴的 module,go.workreplace 更方便,不需要修改 go.mod

# 在上層目錄初始化 workspace
cd ~/projects
go work init ./mylib ./myapp

go.work(自動生成)

go 1.21

use (
    ./mylib
    ./myapp
)

現在 myapp 會自動使用本機 mylib不需要修改任何 go.mod

# 加入更多 module 到 workspace
go work use ./another-module

# 開發完成後,可以完全忽略 go.work(CI 環境不需要它)
GOWORK=off go build ./...

常見錯誤與解法

錯誤 1:no required module provides package

no required module provides package github.com/foo/bar

原因:用到了沒有 go get 的套件。 解法

go get github.com/foo/bar
# 或
go mod tidy  # 自動補上 import 的套件

錯誤 2:imported and not used

./main.go:5:2: "fmt" imported and not used

原因:import 了但沒用到。 解法:刪除該 import,或用空白識別符(只想要 side effect):

import _ "github.com/lib/pq"  // 只執行 init(),不使用其他符號

錯誤 3:package xxx is not in GOROOT

package utils: package utils is not in GOROOT (/usr/local/go/src/utils)

原因:直接寫了 "utils" 而不是 "myapp/utils"解法:import 自己的 package 要加上 module 名:

import "myapp/utils"   // ✅
import "utils"         // ❌

錯誤 4:ambiguous import

ambiguous import: found package github.com/foo/bar in multiple modules

原因go.workreplace 造成同一套件有多個來源。 解法:檢查 go.workgo.modreplace 設定,確保每個 package 只有一個來源。


錯誤 5:cannot find module providing package(Go Proxy 問題)

# 設定 proxy(中國大陸常見問題)
go env -w GOPROXY=https://goproxy.cn,direct

# 關閉 proxy(私有 repo)
go env -w GONOSUMCHECK=your.private.domain
go env -w GOFLAGS=-mod=mod

go mod tidy 實際展示

$ cat go.mod
module myapp

go 1.21

require (
    github.com/google/uuid v1.6.0
    github.com/some/unused v1.0.0   ← 程式碼已不用但還在這
)

$ go mod tidy
# 自動移除 unused,補上 missing

$ cat go.mod
module myapp

go 1.21

require github.com/google/uuid v1.6.0
# some/unused 已被移除

版本語義(SemVer)

Go module 遵守 Semantic Versioning

版本意義範例
v1.2.3正式版go get pkg@v1.2.3
v1.2.3-alpha預覽版go get pkg@v1.2.3-alpha
@latest最新正式版go get pkg@latest
@mainmain 分支最新 commitgo get pkg@main

Major version 規則:v2 以上的 module,import 路徑要加 /v2

import "github.com/foo/bar/v2"  // v2+
import "github.com/foo/bar"      // v0 / v1

完整指令速查

# 初始化
go mod init <模組名>

# 新增套件
go get <套件>@<版本>           # 指定版本
go get <套件>@latest           # 最新版
go get <套件>@main             # main 分支

# 整理
go mod tidy                    # 移除未用、補上缺少

# 查詢
go list -m all                 # 列出所有相依版本
go mod graph                   # 顯示相依樹
go mod why <套件>              # 解釋為什麼需要這個套件

# 下載
go mod download                # 預先下載所有相依

# 離線/vendor
go mod vendor                  # 複製相依到 vendor/
go build -mod=vendor ./...     # 使用 vendor/ 建置

# Workspace
go work init ./a ./b           # 初始化工作區
go work use ./c                # 加入 module
go work sync                   # 同步 go.work.sum