ข้ามไปยังเนื้อหา

Packages และ Imports

ใน Go ทุก source file ต้องเริ่มต้นด้วย package declaration Package คือหน่วยของการจัดระเบียบ code ที่ Go ใช้ควบคุม scope และ visibility

package main

Package name ปกติจะสั้น, lowercase, และตรงกับชื่อ directory (เช่น directory strings → package strings)

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 เพื่อดึง package อื่นเข้ามาใช้ มีสองรูปแบบ:

// single import
import "fmt"
// grouped import — preferred
import (
"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 mux

Go ควบคุม visibility ด้วยตัวพิมพ์ของอักษรตัวแรกของ identifier:

Identifierตัวอย่างVisibility
ขึ้นต้นด้วยตัวพิมพ์ใหญ่fmt.Println, http.HandlerExported — ใช้ได้จาก package อื่น
ขึ้นต้นด้วยตัวพิมพ์เล็กmyHelper, internalMapUnexported — ใช้ได้เฉพาะใน package นั้น

ตัวอย่างเช่น:

package mathutil
// Add is exported — other packages can call mathutil.Add
func Add(a, b int) int { return a + b }
// helper is unexported — invisible outside this package
func helper() {}

กฎนี้ใช้กับ functions, types, variables, constants, และ struct fields ทั้งหมด

ก่อนที่ main จะรัน Go initialize ทั้ง program ตามลำดับที่เข้มงวดและกำหนดไว้ชัดเจน — การเข้าใจลำดับนี้อธิบายได้ว่าทำไม package-level variable ตัวหนึ่งจึงอ้างถึงอีกตัวได้อย่างปลอดภัย และควรวาง setup code ที่ต้องรันก่อนอะไรทั้งหมดไว้ตรงไหน:

  1. Imported package ก่อน ถ้า package ของคุณ import package อื่น package นั้นจะถูก initialize จนเสร็จสมบูรณ์ — ทั้งตัวแปรและ init function ของมันเอง — ก่อนที่ package ของคุณจะเริ่ม initialize เกิดขึ้นแบบ recursive ตาม import graph
  2. Package-level variable ถัดไป ตามลำดับ dependency Go หาว่าตัวแปรไหน depend on ตัวแปรไหน (จากการอ่าน initializer expression) แล้ว initialize ให้ทุก dependency พร้อมก่อนตัวแปรที่ต้องใช้มัน — ไม่จำเป็นต้องเป็นลำดับที่เขียนใน file
  3. init function สุดท้าย ตามลำดับที่ปรากฏ file เดียวประกาศ func init() ได้มากกว่าหนึ่งตัว และ package หนึ่งกระจาย init function ไปหลาย file ได้ ทุกตัวจะรันตามลำดับที่ compiler เจอ (ลำดับ file แล้วตามด้วยลำดับการประกาศใน file) หลังจาก package-level variable ทุกตัว initialize เสร็จแล้ว
  4. 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()"]
ลำดับ initialization: imported package ก่อน แล้วตัวแปรของ package นี้ แล้ว init function แล้วค่อยถึง main
var base = 10
var 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 ตั้งชื่อ 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 อ่านคล้ายภาษาอังกฤษปกติมากขึ้น

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 allowed

error จริงตัวนี้ปรากฏทันทีที่ code จาก นอก module internaldemo พยายาม import internaldemo/internal/secret — แม้ว่า package นั้นจะเป็น Go package ปกติที่มี exported identifier ทุกอย่างครบก็ตาม code ที่อยู่ใน module tree เดียวกัน import มันได้อย่างอิสระ ผู้เขียน library ใช้ internal/ เพื่อแชร์ code ระหว่าง package ของตัวเองโดยไม่ให้มันกลายเป็นส่วนหนึ่งของ public API

ชื่อที่คุณใช้อ้างถึง 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 v2

standard library มีบาง package ที่มี sibling ใหม่กว่าที่ใช้ generics ซึ่งควรรู้ไว้เมื่ออ่าน code ที่มีอายุต่างกัน:

Package เดิมตัวแทนใหม่เหตุผล
math/randmath/rand/v2algorithm default ดีกว่า, API สะอาดกว่า (IntN แทน Intn), ไม่ต้อง seed เพื่อความสุ่มที่ดี
sortslices (Go 1.21)slices.Sort, slices.SortFunc แบบ generic — ไม่ต้องมี boilerplate ของ sort.Interface

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 ระดับสูงเลย

ทุก 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 = 10
var 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))
}

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 errors
  • sort — sort slices และ collections
  • math — math functions (Sqrt, Abs, Round)
  • time — time และ duration operations
  • os — OS interface (file I/O, environment variables)
สิ่งที่ได้ประโยชน์ต้นทุน
uppercase = exportedvisibility rule เรียบง่าย ไม่ต้องใช้ keyword public/privateต้อง rename identifier ถ้าต้องการเปลี่ยน visibility
unused import = compile errorไม่มี dead import ใน codebaseต้องลบ import ที่ไม่ใช้ทุกครั้ง (goimports ช่วยได้)
init() functionsetup เกิดขึ้นอัตโนมัติ ตามลำดับที่กำหนดไว้ชัดเจนhidden side effects ยากต่อการ debug และ test
internal/ packageแชร์ code ระหว่าง package ของตัวเองโดยไม่เปิดเป็น publicทุกอย่างใต้ internal/ module อื่น import ไม่ได้ แม้ภายหลังจะอยากให้ import ได้ก็ตาม
module system (go.mod)reproducible build, explicit dependency, language semantics แยกต่อ modulelearning 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/pq register ตัวเองผ่าน init() ที่เรียก sql.Register("postgres", ...)

net/http/pprof ก็ใช้กลไกเดียวกัน — import _ "net/http/pprof" register handler debug ใต้ /debug/pprof/ ผ่าน init() โดยไม่ต้องเรียกอะไรจาก package นั้นตรง ๆ

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

Package name ใดที่บอกว่า Go file นั้นจะ produce executable binary?
identifier ที่ขึ้นต้นด้วยตัวพิมพ์เล็กใน Go หมายความว่าอะไร?
Go initialize program ก่อน main จะรันตามลำดับใด?
รูปแบบ idiomatic Go เมื่อ import หลาย package คือรูปแบบใด?
โค้ดนี้ compile ผ่านไหม?
import "math/rand/v2"

func main() {
    fmt.Println(v2.IntN(100))
}
Package นอก module internaldemo พยายาม import "internaldemo/internal/secret" จะเกิดอะไร?