กลับไปยังบทความ

Backend

Go Modules และ Packages: go mod init, Exported Names และ go mod tidy

ทำความเข้าใจ Go modules และ packages ตั้งแต่ package main และ func main, go mod init, ชื่อแบบ exported และ unexported, การ import package ภายนอก, go mod tidy, go.sum และพื้นฐานการจัด layout โปรเจกต์

24 กันยายน 25699 นาทีในการอ่าน

go modules and packages

Go modules และ packages คือการจัดระเบียบโค้ดสองระดับของ Go package คือโฟลเดอร์ที่มีไฟล์ .go ซึ่งถูก compile ไปด้วยกันและใช้ namespace เดียวกัน ส่วน module คือชุดของ package ที่มีเวอร์ชัน กำหนดด้วยไฟล์ go.mod ที่ root และเป็นหน่วยที่เราดาวน์โหลด ใส่เวอร์ชัน และประกาศเป็น dependency เมื่อเข้าใจว่าสองอย่างนี้ทำงานร่วมกันอย่างไร เรื่อง import, การมองเห็นชื่อ และการจัดการ dependency จะไม่สับสนอีกต่อไป

บทความนี้พาทำโปรเจกต์เล็ก ๆ ตั้งแต่ go mod init ไปจนถึงการใช้ package จากภายนอก ครอบคลุม package main, ชื่อแบบ exported และ unexported, go mod tidy และพื้นฐานการจัด layout ของโปรเจกต์

package main และ func main

ทุกไฟล์ .go ขึ้นต้นด้วย package และมีชื่อหนึ่งที่พิเศษ คือ package main ซึ่งบอก compiler ว่าให้สร้างเป็นโปรแกรมที่รันได้ และ func main() ใน package นี้คือจุดเริ่มต้นของโปรแกรม

go
package main

import "fmt"

func main() {
	fmt.Println("hello, modules")
}

Package ชื่ออื่นจะกลายเป็น library ที่ package อื่นนำไป import รันเองไม่ได้ ถ้าสั่ง go run จะเจอ error package ... is not a main package

กฎที่ควรรู้:

  • ไฟล์ .go ทุกไฟล์ในโฟลเดอร์เดียวกันต้องประกาศชื่อ package เดียวกัน ยกเว้นไฟล์ test ที่ลงท้าย _test.go ซึ่งใช้ชื่อ <name>_test ได้
  • main ไม่รับ argument และไม่คืนค่า ให้อ่าน argument จาก os.Args หรือ package flag และจบโปรแกรมด้วย status ที่ไม่ใช่ศูนย์ผ่าน os.Exit หรือ log.Fatal
  • ก่อน main จะรัน Go จะ initialize package ที่ถูก import รวมถึงตัวแปรระดับ package และฟังก์ชัน init() ควรใช้ init แค่กับการลงทะเบียนเล็ก ๆ เพราะ side effect ที่ซ่อนอยู่ในนั้น debug ยาก

สร้าง module ด้วย go mod init

สร้างโฟลเดอร์แล้ว initialize module:

bash
mkdir shapes && cd shapes
go mod init example.com/shapes

คำสั่งนี้สร้างไฟล์ go.mod:

text
module example.com/shapes

go 1.25.1
  • module คือ module path ซึ่งเป็น prefix ของทุก import path ภายใน module ถ้าจะเผยแพร่โค้ด ให้ใช้ที่อยู่ของ repository เช่น github.com/your-org/shapes เพื่อให้ go get หาเจอ ถ้าเป็นโปรเจกต์ส่วนตัวหรือทดลอง ใช้ชื่อรูปแบบ domain อะไรก็ได้
  • go คือเวอร์ชัน Go ขั้นต่ำที่ module ต้องการ go mod init จะใส่เวอร์ชันที่ติดตั้งอยู่ให้ ตั้งแต่ Go 1.21 ค่านี้ถูกบังคับใช้จริง toolchain ที่เก่ากว่าจะไม่ยอม build หรือจะดาวน์โหลดเวอร์ชันใหม่มาใช้ ขึ้นกับค่า GOTOOLCHAIN

แบ่งโค้ดเป็น package

ตัวอย่างนี้เป็น module ที่มี library package ชื่อ shape แบ่งเป็นสามไฟล์ และมี main package อยู่ที่ root:

text
shapes/
├── go.mod
├── main.go          # package main
└── shape/
    ├── shape.go     # package shape
    ├── circle.go    # package shape
    └── rectangle.go # package shape
go
// shape/shape.go
package shape

// Shape is anything with an area and a perimeter.
type Shape interface {
	Area() float64
	Perimeter() float64
}
go
// shape/circle.go
package shape

import "math"

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
go
// shape/rectangle.go
package shape

import "fmt"

type Rectangle struct {
	width, height float64 // unexported: only package shape can access them
}

func NewRectangle(w, h float64) (Rectangle, error) {
	if w <= 0 || h <= 0 {
		return Rectangle{}, fmt.Errorf("invalid rectangle %vx%v", w, h)
	}
	return Rectangle{width: w, height: h}, nil
}

func (r Rectangle) Area() float64      { return r.width * r.height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.width + r.height) }

main package import ด้วย module path ต่อด้วยชื่อโฟลเดอร์:

go
// main.go
package main

import (
	"fmt"
	"log"

	"example.com/shapes/shape"
)

func main() {
	rect, err := shape.NewRectangle(3, 4)
	if err != nil {
		log.Fatal(err)
	}
	for _, s := range []shape.Shape{shape.Circle{Radius: 1}, rect} {
		fmt.Printf("%T area=%.2f perimeter=%.2f\n", s, s.Area(), s.Perimeter())
	}
}

รันด้วย go run . ซึ่ง build package ในโฟลเดอร์ปัจจุบัน ส่วน go build จะสร้าง binary และ go build ./... จะ compile ทุก package ใน module

สังเกตว่า circle.go ใช้ Shape จาก shape.go ได้โดยไม่ต้อง import อะไรเลย ไฟล์ใน package เดียวกันใช้ scope ร่วมกัน การแยก package เป็นหลายไฟล์จึงเป็นเรื่องความอ่านง่ายล้วน ๆ

ชื่อแบบ exported และ unexported

Go ไม่มี keyword public หรือ private การมองเห็นขึ้นกับตัวอักษรแรกของชื่อ:

ชื่อมองเห็นได้จากตัวอย่าง
ขึ้นต้นด้วยตัวพิมพ์ใหญ่ทุก package ที่ importCircle, NewRectangle, Area, Radius
ขึ้นต้นด้วยตัวพิมพ์เล็กเฉพาะใน package เดียวกันwidth, height, ฟังก์ชัน helper

กฎนี้ใช้กับ type, function, method, ตัวแปร, ค่าคงที่ และ field ของ struct ขอบเขตคือ package ไม่ใช่ไฟล์ rectangle.go กับ circle.go มองเห็นชื่อตัวพิมพ์เล็กของกันและกัน แต่ main มองไม่เห็น:

go
r, _ := shape.NewRectangle(3, 4)
fmt.Println(r.width)
// compile error: r.width undefined (cannot refer to unexported field width)

นี่คือวิธีทำ encapsulation ของ Go Rectangle ซ่อน field ไว้ ทำให้สร้างได้ทางเดียวคือผ่าน NewRectangle ซึ่งตรวจ input ก่อน ผลข้างเคียงที่ต้องจำคือ encoding/json และ package อื่นที่ใช้ reflection จะมองข้าม field ที่ไม่ได้ export struct ที่ต้องการ serialize จึงต้องมี field แบบ exported พร้อม tag อ่านกลไกเหล่านี้โดยละเอียดได้ใน Go structs, methods และ interfaces

การตั้งชื่อ package

  • ใช้ชื่อสั้น ตัวพิมพ์เล็ก คำเดียว เช่น shape, auth, storage ไม่ใช้ underscore หรือ camelCase
  • ตั้งชื่อโฟลเดอร์ให้ตรงกับชื่อ package แม้ Go จะยอมให้ต่างกันได้ แต่คนอ่านคาดหวังให้ตรงกัน
  • หลีกเลี่ยงการพูดซ้ำ ผู้เรียกเขียน shape.Circle อยู่แล้ว shape.ShapeCircle จึงซ้ำซ้อน
  • หลีกเลี่ยงชื่อครอบจักรวาลอย่าง util, common หรือ helpers เพราะจะดึงโค้ดที่ไม่เกี่ยวกันมารวมไว้ และไม่บอกอะไรเลยตรงจุดที่เรียกใช้

Import package จากภายนอก

ถ้าจะใช้ package จาก module อื่น ให้ import แล้วปล่อยให้คำสั่ง go ดึงมาให้ จะเพิ่ม dependency ตรง ๆ ก็ได้:

bash
go get github.com/google/uuid@latest

หรือเขียน import ก่อนแล้วค่อยรัน go mod tidy:

go
import "github.com/google/uuid"

id := uuid.NewString()

ไม่ว่าทางไหน จะมีสองไฟล์ที่เปลี่ยน:

  • go.mod ได้บรรทัด require เพิ่ม เช่น require github.com/google/uuid v1.6.0 ส่วน dependency ที่เราไม่ได้ import ตรง ๆ จะมี // indirect กำกับ
  • go.sum บันทึก checksum ของแต่ละเวอร์ชัน คำสั่ง go ใช้ตรวจไฟล์ที่ดาวน์โหลดมาเทียบกับไฟล์นี้และกับ checksum database สาธารณะ ต้อง commit ทั้งสองไฟล์

โดย default module จะถูกดาวน์โหลดผ่าน proxy.golang.org ถ้าเป็น repository ส่วนตัว ให้ตั้ง GOPRIVATE=github.com/your-org/* เพื่อให้ดึงตรงจาก repository และข้าม checksum database สาธารณะ

เวอร์ชันใช้ semantic versioning ตั้งแต่ v2 ขึ้นไป major version จะเป็นส่วนหนึ่งของ import path เช่น github.com/go-playground/validator/v10 ทำให้สอง major version อยู่ใน build เดียวกันได้ เมื่อ dependency หลายตัวต้องการ minor version ต่างกันของ module เดียวกัน Go จะเลือกเวอร์ชันต่ำสุดที่ตอบโจทย์ทุกตัว (minimal version selection) build จึง reproducible โดยไม่ต้องมี lock file แยก

เก็บกวาดด้วย go mod tidy

Go ไม่ยอม compile ไฟล์ที่มี import ซึ่งไม่ได้ใช้ แต่ไม่มีอะไรห้าม go.mod ไม่ให้เก็บ module ที่เลิกใช้ไปแล้ว go mod tidy ปรับ go.mod และ go.sum ให้ตรงกับ import ในโค้ด คือเพิ่มสิ่งที่ขาด และลบ requirement ที่ไม่มีใคร import รันทุกครั้งหลังเพิ่มหรือลบ import และใน CI ให้ตรวจว่ารันแล้วไม่มี diff

คำสั่งอื่นที่จะได้ใช้:

คำสั่งหน้าที่
go get example.com/[email protected]เพิ่มหรือเปลี่ยน dependency เป็นเวอร์ชันที่ระบุ
go get -u ./...อัปเกรด dependency เป็น minor และ patch version ที่ใหม่กว่า
go get example.com/pkg@noneลบ dependency
go list -m allแสดงทุก module ใน build
go list -m -u allแสดง module ที่มีเวอร์ชันใหม่
go mod why example.com/pkgอธิบายว่าทำไมต้องใช้ module นี้
go mod verifyตรวจ module cache เทียบกับ go.sum

สำหรับเครื่องมือของนักพัฒนา เช่น code generator ตั้งแต่ Go 1.24 มี directive tool แล้ว go get -tool golang.org/x/tools/cmd/stringer จะบันทึกไว้ใน go.mod และ go tool stringer จะรันเวอร์ชันที่ pin ไว้

ทำงานกับสอง module พร้อมกัน

เมื่อต้องแก้ library และแอปไปพร้อมกัน ให้แอปชี้ไปที่ library ในเครื่อง directive replace ใน go.mod ทำได้ แต่เผลอ commit ขึ้นไปง่าย workspace ช่วยให้ override นี้อยู่นอก go.mod:

bash
go work init ./app ./shapes

คำสั่งนี้สร้างไฟล์ go.work ซึ่งทีมส่วนใหญ่ไม่เก็บเข้า version control

พื้นฐานการจัด layout ของโปรเจกต์

Module เล็ก ๆ วางแบบแบนได้ คือ main.go อยู่ที่ root และมี package ไม่กี่ตัวอยู่ข้าง ๆ เมื่อโปรเจกต์โตขึ้น มี convention สองอย่างที่ช่วยได้:

text
myservice/
├── go.mod
├── cmd/
│   ├── api/main.go      # one directory per binary
│   └── worker/main.go
└── internal/
    ├── article/         # importable only inside myservice
    └── storage/
  • cmd/<name>/ เก็บ package main หนึ่งตัวต่อหนึ่งโปรแกรม และควรบางที่สุด คืออ่าน config ประกอบ dependency แล้ว start
  • internal/ ถูกบังคับโดย compiler package ที่อยู่ข้างในจะถูก import ได้เฉพาะจากโค้ดที่อยู่ใต้ parent ของ internal เท่านั้น module อื่นจึงพึ่งพาส่วนภายในของเราไม่ได้

ไม่จำเป็นต้องมีโฟลเดอร์ pkg/ หรือโครงสร้างที่ลึกหลายชั้น ให้จัด package ตามสิ่งที่มันทำ ไม่ใช่ตาม layer ทางเทคนิค Go error handling และ project layout อธิบายว่า layout และ error handling เติบโตไปด้วยกันอย่างไร และ คู่มือสร้าง REST API ด้วย Gin และ GORM แสดงการใช้กฎเหล่านี้กับ service จริง

คำถามที่พบบ่อย

Go module กับ package ต่างกันอย่างไร

Package คือโฟลเดอร์ของโค้ดที่ compile ไปด้วยกัน ส่วน module คือชุดของ package ที่มีเวอร์ชันและมี go.mod ที่ root เรา import เป็น package แต่ใส่เวอร์ชันและดาวน์โหลดเป็น module

go mod tidy ทำอะไร

เพิ่ม requirement ที่ขาดสำหรับ package ที่ import ลบ requirement ที่ไม่มีใคร import และอัปเดต go.sum ให้ตรงกัน

ควร commit go.sum ไหม

ควร เพราะทำให้ build ตรวจสอบได้และ reproducible ให้ commit คู่กับ go.mod เสมอ

go get กับ go install ต่างกันอย่างไร

go get เปลี่ยน dependency ใน go.mod ของ module ปัจจุบัน ส่วน go install example.com/cmd@latest build และติดตั้ง binary ลงใน $GOBIN โดยไม่แตะ go.mod

เช็กลิสต์

  • มี go.mod หนึ่งไฟล์ที่ root ของ repository และ module path ตรงกับที่อยู่ของโค้ด
  • หนึ่งโฟลเดอร์คือหนึ่ง package และตั้งชื่อตามโฟลเดอร์
  • เฉพาะชื่อที่ package อื่นต้องใช้เท่านั้นที่ขึ้นต้นด้วยตัวพิมพ์ใหญ่
  • เพิ่ม dependency ด้วย go get หรือ import แล้ว go mod tidy และ commit ทั้ง go.mod และ go.sum
  • โปรแกรมที่รันได้อยู่ใน cmd/ และโค้ดภายในอยู่ใน internal/

การวาง module และ package ให้ถูกตั้งแต่แรกทำให้การ refactor ทุกครั้งหลังจากนั้นถูกลง ถ้าทีมของคุณกำลังเริ่ม codebase ภาษา Go และอยากได้คนช่วยวางโครงสร้าง Vectorkub ช่วยได้

พร้อมสร้างสิ่งที่ยิ่งใหญ่หรือยัง?

เล่าโปรเจคของคุณให้เราฟัง — เราจะพาคุณไปถึง

เลือกคุกกี้ที่เราใช้ได้ คุกกี้ที่จำเป็นช่วยให้เว็บไซต์ทำงานได้และเปิดใช้งานเสมอ