Packages และ Imports
Package คืออะไร?
หัวข้อที่มีชื่อว่า “Package คืออะไร?”ใน Go ทุก source file ต้องเริ่มต้นด้วย package declaration Package คือหน่วยของการจัดระเบียบ code ที่ Go ใช้ควบคุม scope และ visibility
package mainPackage name ปกติจะสั้น, lowercase, และตรงกับชื่อ directory (เช่น directory strings → package strings)
func main — จุดเริ่มต้น
หัวข้อที่มีชื่อว่า “func main — จุดเริ่มต้น”Program ที่ executable ต้องมี package main และ function func main() เสมอ เมื่อคุณรัน binary Go จะเริ่มต้นที่ main.main()
package main
func main() { // execution starts here}Package อื่น ๆ (เช่น library) ไม่มี func main แต่ถูก import ไปใช้โดย package อื่น
การ Import
หัวข้อที่มีชื่อว่า “การ Import”ใช้ import เพื่อดึง package อื่นเข้ามาใช้ มีสองรูปแบบ:
// single importimport "fmt"
// grouped import — preferredimport ( "fmt" "strings" "strconv")Go compiler จะ error ทันทีถ้าคุณ import package แล้วไม่ใช้ สิ่งนี้บังคับให้ code สะอาด
หากต้องการ import package เพื่อ side effect เท่านั้น (เช่น init() ของ package นั้น) ใช้ blank identifier:
import _ "github.com/lib/pq" // the driver's init() calls sql.Register("postgres", ...)อีกตัวอย่างที่พบบ่อยคือเปิด debug endpoint โดยไม่ได้เรียกอะไรจาก package นั้นตรง ๆ:
import _ "net/http/pprof" // registers /debug/pprof/ handlers on the default muxExported vs Unexported
หัวข้อที่มีชื่อว่า “Exported vs Unexported”Go ควบคุม visibility ด้วยตัวพิมพ์ของอักษรตัวแรกของ identifier:
| Identifier | ตัวอย่าง | Visibility |
|---|---|---|
| ขึ้นต้นด้วยตัวพิมพ์ใหญ่ | fmt.Println, http.Handler | Exported — ใช้ได้จาก package อื่น |
| ขึ้นต้นด้วยตัวพิมพ์เล็ก | myHelper, internalMap | Unexported — ใช้ได้เฉพาะใน package นั้น |
ตัวอย่างเช่น:
package mathutil
// Add is exported — other packages can call mathutil.Addfunc Add(a, b int) int { return a + b }
// helper is unexported — invisible outside this packagefunc helper() {}กฎนี้ใช้กับ functions, types, variables, constants, และ struct fields ทั้งหมด
ลำดับการ Initialize Program
หัวข้อที่มีชื่อว่า “ลำดับการ Initialize Program”ก่อนที่ main จะรัน Go initialize ทั้ง program ตามลำดับที่เข้มงวดและกำหนดไว้ชัดเจน — การเข้าใจลำดับนี้อธิบายได้ว่าทำไม package-level variable ตัวหนึ่งจึงอ้างถึงอีกตัวได้อย่างปลอดภัย และควรวาง setup code ที่ต้องรันก่อนอะไรทั้งหมดไว้ตรงไหน:
- Imported package ก่อน ถ้า package ของคุณ import package อื่น package นั้นจะถูก initialize จนเสร็จสมบูรณ์ — ทั้งตัวแปรและ
initfunction ของมันเอง — ก่อนที่ package ของคุณจะเริ่ม initialize เกิดขึ้นแบบ recursive ตาม import graph - Package-level variable ถัดไป ตามลำดับ dependency Go หาว่าตัวแปรไหน depend on ตัวแปรไหน (จากการอ่าน initializer expression) แล้ว initialize ให้ทุก dependency พร้อมก่อนตัวแปรที่ต้องใช้มัน — ไม่จำเป็นต้องเป็นลำดับที่เขียนใน file
initfunction สุดท้าย ตามลำดับที่ปรากฏ file เดียวประกาศfunc init()ได้มากกว่าหนึ่งตัว และ package หนึ่งกระจายinitfunction ไปหลาย file ได้ ทุกตัวจะรันตามลำดับที่ compiler เจอ (ลำดับ file แล้วตามด้วยลำดับการประกาศใน file) หลังจาก package-level variable ทุกตัว initialize เสร็จแล้วmainรันสุดท้ายสุด และเฉพาะในpackage mainเท่านั้น
flowchart TB
subgraph Imported["Each imported package (recursively, dependency order)"]
IV["its package-level vars"] --> II["its init() funcs"]
end
Imported --> V["this package's vars, in dependency order"]
V --> I["this package's init() funcs, in file/declaration order"]
I --> Main["main()"] var base = 10var derived = base * 2 // base is guaranteed initialized first
func init() { fmt.Println("init #1: derived =", derived)}
func init() { fmt.Println("init #2: a file can have more than one init()")}Import Alias และ Dot Import
หัวข้อที่มีชื่อว่า “Import Alias และ Dot Import”Import ตั้งชื่อ local ให้ได้ ซึ่งมีประโยชน์เมื่อ package ที่ import สอง package มีชื่อ default ชนกัน หรือแค่อยากได้ชื่อสั้นลง:
import ( f "fmt")
f.Println("aliased import")Dot import — import . "strings" — ดึงทุก exported identifier ของ package นั้นเข้ามาที่ scope ของ file ปัจจุบันตรง ๆ ทำให้เรียก ToUpper(...) แทน strings.ToUpper(...) ได้ มัน compile และรันได้ปกติ แต่แทบไม่ใช้ใน production code เลย: มันทำให้บอกไม่ได้ว่า identifier มาจาก package ไหนแค่อ่านจากจุดที่เรียกใช้ และเสี่ยงชนกับชื่อที่มีอยู่แล้วใน scope อย่างเงียบ ๆ ที่เดียวที่มันปรากฏใช้จริงคือใน test file บางไฟล์ เพื่อให้ table-driven assertion อ่านคล้ายภาษาอังกฤษปกติมากขึ้น
internal/ Package
หัวข้อที่มีชื่อว่า “internal/ Package”Package ที่ import path มี directory ชื่อ internal อยู่ในนั้นตรง ๆ จะ import ได้เฉพาะจาก code ที่อยู่ใน module ที่ root อยู่ที่ parent ของ directory internal นั้นเท่านั้น สิ่งนี้ compiler บังคับใช้จริง ไม่ใช่แค่ convention:
main.go:5:2: use of internal package internaldemo/internal/secret not allowederror จริงตัวนี้ปรากฏทันทีที่ code จาก นอก module internaldemo พยายาม import internaldemo/internal/secret — แม้ว่า package นั้นจะเป็น Go package ปกติที่มี exported identifier ทุกอย่างครบก็ตาม code ที่อยู่ใน module tree เดียวกัน import มันได้อย่างอิสระ ผู้เขียน library ใช้ internal/ เพื่อแชร์ code ระหว่าง package ของตัวเองโดยไม่ให้มันกลายเป็นส่วนหนึ่งของ public API
Package Name กับ Import Path
หัวข้อที่มีชื่อว่า “Package Name กับ Import Path”ชื่อที่คุณใช้อ้างถึง package ใน code คือ package name — identifier หลัง keyword package ใน source file ของมัน — ซึ่งไม่ได้ตรงกับ element สุดท้ายของ import path เสมอไป math/rand/v2 คือตัวอย่างที่ชัดที่สุด: import path ของมันลงท้ายด้วย /v2 (major version suffix ส่วนหนึ่งของ convention การ version module ของ Go สำหรับ breaking change) แต่ทุก file ในนั้นยังประกาศ package rand ดังนั้นคุณเขียน rand.IntN(...) ไม่ใช่ v2.IntN(...):
import "math/rand/v2"
rand.IntN(100) // the package is called rand, not v2standard library มีบาง package ที่มี sibling ใหม่กว่าที่ใช้ generics ซึ่งควรรู้ไว้เมื่ออ่าน code ที่มีอายุต่างกัน:
| Package เดิม | ตัวแทนใหม่ | เหตุผล |
|---|---|---|
math/rand | math/rand/v2 | algorithm default ดีกว่า, API สะอาดกว่า (IntN แทน Intn), ไม่ต้อง seed เพื่อความสุ่มที่ดี |
sort | slices (Go 1.21) | slices.Sort, slices.SortFunc แบบ generic — ไม่ต้องมี boilerplate ของ sort.Interface |
Circular Import
หัวข้อที่มีชื่อว่า “Circular Import”Import graph ของ Go ต้องไม่มี cycle: ถ้า package a import package b, package b จะ import a ไม่ได้ ไม่ว่าตรง ๆ หรือผ่าน chain ของ package อื่น compiler ปฏิเสธทันที:
package circdemo/a imports circdemo/b from a.go imports circdemo/a from b.go: import cycle not allowedวิธีแก้มาตรฐานคือแยกส่วนที่ทั้งสอง package ต้องใช้ร่วมกันออกไปเป็น package ระดับล่างตัวที่สาม ที่ทั้งสอง package เดิม import มาใช้ — หรือถ้า dependency มีทิศทางเดียวในเชิง concept ก็ให้ package ระดับสูงกว่านิยาม interface ที่ package ระดับล่างกว่า implement แทน ตัด requirement ที่ package ระดับล่างต้อง import อะไรจาก package ระดับสูงเลย
go.mod ในหนึ่งย่อหน้า
หัวข้อที่มีชื่อว่า “go.mod ในหนึ่งย่อหน้า”ทุก module มีไฟล์ go.mod ที่ root ประกาศ import path ของ module (module example.com/myapp), dependency พร้อม version ที่ pin ไว้, และ go directive (go 1.27) ที่บันทึก version ภาษาต่ำสุดที่ module นั้นต้องการ go directive นี้ไม่ใช่แค่ documentation: compiler อ่านมันเพื่อตัดสินใจว่า language semantics แบบไหนจะใช้กับ code ของ module นั้นเอง — ที่สำคัญที่สุดคือมันเป็นตัว gate การเปลี่ยน loop-variable แบบต่อรอบของ Go 1.22 ที่พูดถึงในบท Functions ดังนั้น code ใน module ที่ยังประกาศ go 1.21 จะยังคง behavior แบบ shared-loop-variable ก่อน 1.22 ไว้ แม้จะ build ด้วย Go toolchain ที่ใหม่กว่ามากก็ตาม
package main
import ( "fmt" "strconv" "strings")
func main() { // fmt: formatted output fmt.Println("Go packages demo")
// strings: manipulating text sentence := "the quick brown fox" words := strings.Split(sentence, " ") fmt.Println("Word count:", len(words)) fmt.Println("Uppercase:", strings.ToUpper(sentence)) fmt.Println("Contains 'fox':", strings.Contains(sentence, "fox"))
// strconv: converting between strings and numbers n := 42 s := strconv.Itoa(n) fmt.Println("int to string:", s)
parsed, err := strconv.Atoi("123") if err == nil { fmt.Println("string to int:", parsed) }
// Combining packages together parts := []string{"Go", strconv.Itoa(1), strconv.Itoa(22)} fmt.Println("Joined:", strings.Join(parts, "."))} Playground ตัวที่สองสาธิตลำดับ initialization, import alias, dot import, และ package ที่ชื่อไม่ตรงกับ import path ของมัน
package main
import ( f "fmt" . "strings" "math/rand/v2")
// Package-level variables initialize first, in dependency order.var base = 10var derived = base * 2 // depends on base, so base is guaranteed ready first
func init() { f.Println("init #1: derived =", derived)}
func init() { f.Println("init #2: a file can have more than one init()")}
func main() { f.Println("main: base =", base, "derived =", derived)
// f is an import alias for fmt. f.Println("aliased import works:", ToUpper("dot import too")) // ToUpper comes from the dot-imported strings package
// math/rand/v2's import path ends in /v2, but its package name is rand. f.Println("rand.IntN(1) =", rand.IntN(1))} Standard Library
หัวข้อที่มีชื่อว่า “Standard Library”Go มี standard library ที่ครบครันในตัว package ที่ใช้บ่อย ได้แก่:
fmt— formatted I/O (Println, Printf, Sprintf, Errorf)strings— string manipulation (ToUpper, Split, Join, Contains, Replace)strconv— แปลงระหว่าง string และ numeric types (Itoa, Atoi, ParseFloat)errors— สร้างและ wrap errorssort— sort slices และ collectionsmath— math functions (Sqrt, Abs, Round)time— time และ duration operationsos— OS interface (file I/O, environment variables)
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| uppercase = exported | visibility rule เรียบง่าย ไม่ต้องใช้ keyword public/private | ต้อง rename identifier ถ้าต้องการเปลี่ยน visibility |
| unused import = compile error | ไม่มี dead import ใน codebase | ต้องลบ import ที่ไม่ใช้ทุกครั้ง (goimports ช่วยได้) |
init() function | setup เกิดขึ้นอัตโนมัติ ตามลำดับที่กำหนดไว้ชัดเจน | hidden side effects ยากต่อการ debug และ test |
internal/ package | แชร์ code ระหว่าง package ของตัวเองโดยไม่เปิดเป็น public | ทุกอย่างใต้ internal/ module อื่น import ไม่ได้ แม้ภายหลังจะอยากให้ import ได้ก็ตาม |
| module system (go.mod) | reproducible build, explicit dependency, language semantics แยกต่อ module | learning curve สูงกว่า GOPATH เดิม |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “circular import ใช้ได้ใน test files” — Go ไม่อนุญาต circular import ไม่ว่าจะเป็น production code หรือ test files
- “
import _ใช้ได้แค่กับ database driver” — ใช้ได้กับทุก package ที่ต้องการให้init()ทำงาน เช่น register codec, plugin, หรือ HTTP handler - “Go module คือแค่ package manager เหมือน npm” — Go module จัดการ versioning และ reproducible build ที่ระดับ module ทั้งหมด ไม่ใช่แค่ package เดี่ยว
- “ชื่อ package ตรงกับ element สุดท้ายของ import path เสมอ” —
math/rand/v2คือpackage rand;/v2คือ version suffix ใน path ไม่ใช่ส่วนหนึ่งของชื่อ - “
internal/เป็นแค่ naming convention เหมือน underscore นำหน้า” — compiler บังคับใช้จริง การ import internal package จากนอก module tree เป็น compile error ไม่ใช่แค่ lint warning
💡 ตัวอย่างจากของจริง
database/sqlใช้ blank import ให้ driver เช่นgithub.com/lib/pqregister ตัวเองผ่านinit()ที่เรียกsql.Register("postgres", ...)
net/http/pprofก็ใช้กลไกเดียวกัน —import _ "net/http/pprof"register handler debug ใต้/debug/pprof/ผ่านinit()โดยไม่ต้องเรียกอะไรจาก package นั้นตรง ๆ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”Generated code ที่ประกาศ package, import, หรือ init function มักซ่อนปัญหาพวกนี้:
init()function ทำสิ่งที่จะชัดเจน ปลอดภัย หรือ test ง่ายกว่าถ้าเป็น function ธรรมดาที่เรียกจากmainแทนหรือเปล่า? ไม่ใช่init()ทุกตัวที่คุ้มค่ากับความ implicit ของมัน- initializer ของ package-level variable เรียก function ที่มี side effect (network call, อ่านไฟล์) หรือเปล่า? สิ่งนั้นจะรันก่อน
mainจะเริ่มด้วยซ้ำ โดยไม่มีทางจัดการ error จากมันได้ - dot import (
import . "pkg") ถูกใช้นอก test file หรือเปล่า? มันซ่อนว่า identifier มาจากไหนและเสี่ยงชื่อชนกันแบบเงียบ ๆ - import path ที่ลงท้ายด้วย
/v2(หรือสูงกว่า) ถูกเรียกว่าv2.Somethingใน code แทนที่จะเป็นชื่อจริงของ package หรือเปล่า? - code พยายาม import อะไรที่อยู่ใต้ directory
internal/จากนอก module นั้นหรือเปล่า? นั่นเป็น compile error ไม่ใช่ runtime surprise — แต่ generated code บางครั้งสมมติว่า internal helper ใช้ซ้ำข้าม project ได้
🐛 หา bug ในโค้ดนี้
import "math/rand/v2"
func main() { fmt.Println(v2.IntN(100))}เฉลย
Import path math/rand/v2 ลงท้ายด้วย /v2 แต่ชื่อจริงของ package — ที่ประกาศด้วย package rand ใน source ของมันเอง — คือ rand ไม่ใช่ v2 compiler error จริงคือ undefined: v2 วิธีแก้คืออ้างถึงมันด้วยชื่อจริง: rand.IntN(100) suffix /v2 ใน import path เป็น convention การ version module ของ Go สำหรับ breaking change ของ package เชิง logic เดียวกัน มันไม่ใช่ส่วนหนึ่งของ identifier ที่คุณใช้ใน code