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

Go ตัดสิน​ใจ​ต่าง​ตรง​ไหน และ​เครื่อง​มือ​ชุด​แรก

คุณ​เขียน​ภาษา​อื่น​เป็น​อยู่​แล้ว คอร์ส​นี้​จึง​ไม่​เริ่ม​ที่​ว่า​ตัวแปร​คือ​อะไร

มัน​เริ่ม​ที่​คำถาม​ที่​มี​ประโยชน์​กว่า — Go ตัดสิน​ใจ​ต่าง​จาก​ภาษา​ที่​คุณ​ใช้​อยู่​ตรง​ไหน และ​มัน​จ่าย​อะไร​ไป​กับ​การ​ตัดสิน​ใจ​นั้น

📦 orderctl

คอร์ส​นี้​สร้าง​ของ​ชิ้น​เดียว​ไล่​ยาว​แปดบท — orderctl เครื่อง​มือ​บรรทัด​คำ​สั่ง​ที่​ประมวล​ผล ออ​เด​อร์​อาหาร อ่าน​จาก JSON ตรวจ​ความ​ถูก​ต้อง คิด​ส่วนลด เก็บ​สถานะ แล้ว​บท​สุดท้าย​เปิด HTTP

ใช้ standard library ล้วน ไม่มี framework ไม่มี ORM · บท​นี้​เป็น​บท​แนวคิด​กับ​การ​ตั้ง​ค่า code ที่​รัน​ได้​จริง​มี​แค่ hello world ของ​จริง​เริ่ม​บท​ที่ 2

Java กับ Kotlin จะ​สร้าง​ของ​ชิ้น​เดียวกัน​นี้​ใน​คอร์ส​ถัด​ไป เพื่อ​ให้​คุณ​เทียบ​สาม​ภาษา​ได้​ตรงๆ

Go ตัดสิน​ใจ น้อย​กว่า ภาษา​ส่วน​ใหญ่ และ​สาม​ข้อ​นี้​คือ​ที่​ที่​คน​ย้าย​มา​สะดุด​ก่อน​เสมอ

สิ่ง​ที่​ภาษา​ของ​คุณ​มีGo ให้​แทนราคา​ที่​จ่าย
try/catch และ exception ที่​พา​ตัว​เอง​ขึ้น​ไป​หา​ผู้​เรียกerror เป็น​ค่า​ธรรมดา​ที่​ฟังก์ชัน​คืน​กลับ​มาคุณ​ต้อง​เขียน​ว่า​จะ​ทำ​อะไร​กับ​มัน​ทุก​จุด​ที่​มัน​เกิด ไม่มี​การก​ระ​โดด​ข้าม
การ​สืบทอด class และ virtual methodการ​ฝัง​ชนิด​อื่น​เข้าไป แล้ว​ส่ง​ต่อ method ให้ไม่มี override ไม่มี template method — ต้อง​ออกแบบ​ด้วย​การ​ประกอบ​แทน
ตัว​เลือก​ของ formatter ที่​ทีม​ต้อง​ถก​กันgofmt ที่​ไม่มี​ตัว​เลือก​สไตล์​ให้​ตั้งคุณ​เลือก​รูป​แบบ​ไม่​ได้​เลย แม้แต่​ข้อ​ที่​คุณ​คิด​ว่า​ดี​กว่า

สาม​แถว​นี้​ไม่ใช่​ข้อดี​อย่าง​เดียว มัน​คือ​การ​แลก · Go ยอม​เสีย​ความ​ยืดหยุ่น​เพื่อ​ให้ code ที่​คน​อื่น​เขียน อ่าน​ออก​โดย​ไม่​ต้อง​เรียน​สไตล์​ของ​ทีม​นั้น​ก่อน

ทุก​บท​ของ​คอร์ส​นี้​จะ​มี​อย่าง​น้อยห​นึ่ง​จุด​ที่​บอก​ว่า สิ่ง​ที่​คุณ​คาด​ว่า​จะ​เกิด กับ​สิ่ง​ที่​เกิด​จริง ต่าง​กัน​ตรง​ไหน เพราะ​จุด​ที่​เจ็บ​ที่สุด​เวลา​ย้าย​ภาษา ไม่ใช่​สิ่ง​ที่​ไม่รู้ แต่​เป็น​สิ่ง​ที่​คิด​ว่า​รู้​แล้ว

go ตัว​เดียว​ทำงาน​ที่​ภาษา​อื่น​แบ่ง​ให้​เครื่อง​มือ​สาม​สี่​ตัว

ติด​ตั้ง​จาก go.dev/dl แล้ว​ตรวจ​ว่า​ติด​จริง

$ go version
go version go1.27.0 darwin/arm64

คำ​สั่ง​ที่​คุณ​จะ​ใช้​ทุก​วัน​มี​ไม่​กี่​ตัว

  • go run แปล​แล้ว​รัน​ทันที ไม่​ทิ้ง​ผลลัพธ์​ไว้
  • go build แปล​เป็น​โปรแกรม​ที่​รัน​ได้​หนึ่ง​ตัว ไม่​ต้อง​มี runtime ติด​ไป​ด้วย
  • go test รัน​เทสต์ · ตัว​รัน​เทสต์​อยู่​ใน​กล่อง​เดียวกัน ไม่​ต้อง​ลง library เพิ่ม
  • go vet ตรวจ​รูป​แบบ​ที่​คอม​ไพล์​ผ่าน​แต่​แทบ​ไม่มี​ทาง​เป็น​สิ่ง​ที่​ตั้งใจ
  • gofmt จัด​รูป​แบบ · ไม่มี​ตัว​เลือก​สไตล์​ให้​ตั้ง

เทียบ​กับ​ที่​คุณ​รู้​อยู่ ทุก​แถว​เป็น​สะพาน ไม่ใช่​ของ​เท่า​กัน​เป๊ะ

Goที่​คุณ​คุ้นจุด​ที่​ต่าง
godotnet CLI · Maven หรือ Gradle · npm scriptsตัว​เดียว​คุม​ทั้ง build, test, dependency และ​การ​แจก​จ่าย ไม่มี plugin ให้​ตั้ง
go builddotnet publish · mvn packageได้​โปรแกรม​เดียว​ที่​รัน​บน​เครื่อง​เปล่า​ได้ ไม่​ต้อง​มี runtime หรือ JVM รอ​อยู่
gofmtPrettier · EditorConfig · dotnet-formatไม่มี​ค่า​ให้​ตั้ง จึง​ไม่มี​อะไร​ให้​ถก​กัน​ใน​รีวิว
go.mod.csproj · pom.xml · package.jsonหนึ่ง file ต่อ​หนึ่ง module ไม่ใช่​ต่อ​หนึ่ง project และ​ไม่มี​ราย​ชื่อ file อยู่​ข้าง​ใน
ไม่มี file ไหน​ต้อง​ประกาศ​ว่า​มี file อะไร​บ้าง

ถ้า​คุณ​มา​จาก C# หรือ Java คุณ​จะ​เผลอ​มอง​หา​ที่​ที่​ต้อง​ลง​ทะเบียน file ใหม่

Go ไม่มี​ที่​นั้น · หน่วย​ของ​การ​แปล​คือ ไดเรกทอรี — ทุก file ใน​ไดเรกทอรี​เดียวกัน ที่​ประกาศ​ชื่อ package ตรง​กัน คือ package เดียวกัน​โดย​อัตโนมัติ

ผล​ที่​ตาม​มา​คือ​การ​เพิ่ม file ไม่​ต้อง​แตะ​อะไร​เลย แต่​การ​วาง​ของ​ผิด​ไดเรกทอรี​ก็​เงียบ​เหมือน​กัน

go mod init สร้าง​สิ่ง​เดียว​ที่​คอร์ส​นี้​ต้องการ​เพื่อ​เริ่ม​ต้น

modulemoduleหน่วย​การ​แจก​จ่าย​ของ Go หนึ่ง module มี `go.mod` หนึ่ง file ที่​ราก ประกาศ module path และ​รายการ​ที่​พึ่งพา — ไม่ใช่​หนึ่ง file ต่อ​หนึ่ง package แบบ .csproj คือ​หน่วย​การ​แจก​จ่าย​ของ Go · หนึ่ง module มี go.mod หนึ่ง file ที่​ราก และ​ครอบ​ทุก​ไดเรกทอรี​ที่​อยู่​ข้าง​ใต้

$ go mod init example.com/orderctl
go: creating new go.mod: module example.com/orderctl
$ cat go.mod
module example.com/orderctl
go 1.27.0

สอง​บรรทัด​นี้​ทำงาน​คนละ​อย่าง​กัน​สิ้น​เชิง

บรรทัด module คือ​ชื่อ​ที่​คน​อื่น​ใช้ import เข้า​มา · มัน​ไม่​ต้อง​เป็น​ที่​อยู่​จริง​บน​อินเทอร์เน็ต แต่​ถ้า​คุณ​จะ​เผย​แพร่ มัน​ควร​ตรง​กับ​ที่​ที่ code อยู่​จริง

บรรทัด go คือ​ส่วน​ที่​คน​เข้าใจ​ผิด​กัน​มาก​ที่สุด และ​หัวข้อ​ถัด​ไป​มี​ไว้​เพื่อ​ข้อ​นี้​ข้อ​เดียว

มัน​คือ​กฎ​ภาษา​ที่ compiler บังคับ — เปลี่ยน​ตัวเลข แล้ว​โปรแกรม​เดิม​ให้​ผล​คนละ​อย่าง

go directivego directiveบรรทัด `go` ใน `go.mod` เป็น​ทั้ง​ขั้น​ต่ำ​ที่​ผู้​ใช้ module ต้อง​มี **และ** เป็น​กฎ​ภาษา​ที่ compiler บังคับ — ไม่ใช่​ป้าย​บอก​รุ่น​ของ toolchain ที่​รัน​อยู่ ทำ​สอง​หน้าที่​พร้อม​กัน มัน​บอก​ว่า​ผู้​ใช้ module นี้​ต้อง​มี Go อย่าง​น้อย​รุ่น​ไหน และ มัน​เลือก​ชุด​กฎ​ภาษา​ที่ compiler จะ​ใช้​กับ code ใน​นี้

ข้อ​หลัง​คือ​ข้อ​ที่​ไม่มี​ใคร​คาด · ลอง​ดู​ของ​จริง

package main
import "fmt"
func main() {
var fns []func()
for i := 0; i < 3; i++ {
fns = append(fns, func() { fmt.Print(i, " ") })
}
for _, f := range fns {
f()
}
fmt.Println()
}

โปรแกรม​นี้​เก็บ​ฟังก์ชัน​สาม​ตัว​ไว้ แล้ว​เรียก​ทีหลัง · แต่ละ​ตัว​พิมพ์​ค่า i ที่​มัน​เห็น

รัน​สอง​รอบ แก้​แค่​ตัวเลข​ใน​บรรทัด go ของ go.mod โดย​ไม่​แตะ​โปรแกรม​เลย​สัก​ตัว​อักษร

บรรทัด​ใน go.modผล​ที่​ได้เพราะ​อะไร
go 1.213 3 3ตัวแปร i มี​ตัว​เดียว​ตล​อด​ทั้ง​ลูป ฟังก์ชัน​ทั้ง​สาม​จึง​ชี้​ไป​ที่​ตัว​เดียวกัน
go 1.25.00 1 2ตั้งแต่ Go 1.22 ตัว​แปร​ของ​ลูป​ถูก​สร้าง​ใหม่​ทุกร​อบ แต่ละ​ฟังก์ชัน​จึง​ได้​ตัว​ของ​ตัว​เอง

toolchain ที่​รัน​คือ​ตัว​เดียวกัน​ทั้ง​สอง​รอบ ผล​ที่​ต่าง​ไม่​ได้​มา​จาก​การ​เปลี่ยน compiler แต่​มา​จาก​บรรทัด​เดียว​ใน go.mod

ทำไม​ข้อ​นี้​ถึง​กัด​คน​ที่​อ่าน​ตัวอย่าง​เก่า

ตัวอย่าง​จาก​ยุค​ก่อน Go 1.22 มัก​มี​บรรทัด i := i ยัด​ไว้​ต้น​ลูป เพื่อ​สร้าง​ตัวแปร​ใหม่​ด้วย​มือ

ถ้า​คุณ​ลอก​บรรทัด​นั้น​มา​ใช้​กับ module ที่​ตั้ง go ไว้​ที่ 1.22 ขึ้น​ไป มัน​ไม่​ผิด แต่​มัน​ไม่​จำเป็น และ​มัน​บอก​คน​อ่าน​ผิด​ว่า​ยัง​มี​ปัญหา​นั้น​อยู่

ตรง​ข้าม ถ้า​คุณ​ลอก code ที่ ไม่มี บรรทัด​นั้น ไป​วาง​ใน module ที่​ตั้ง go ไว้​ต่ำ​กว่า 1.22 คุณ​จะ​ได้ bug ที่ compiler ไม่​เตือน และ​เทสต์​ที่​เขียน​หลวมๆ ก็​จับ​ไม่​ได้

คอร์ส​นี้​ตั้ง​ขั้น​ต่ำ​ไว้​ที่ 1.25.0 · สั่ง​แบบ​นี้ ไม่​ต้อง​พิมพ์​ตัวเลข​ลง go.mod เอง

$ go get go@1.25.0
go: downgraded go 1.27.0 => 1.25.0

orderctl เริ่ม​จาก file เดียว​ที่​ยัง​ไม่​ทำ​อะไร​เลย

สร้าง​ไดเรกทอรี​แล้ว​วาง file นี้​ที่ cmd/orderctl/main.go

package main
import "fmt"
func main() {
fmt.Println("orderctl พร้อมทำงาน")
}

package mainpackage mainpackage ที่​มี​ฟังก์ชัน `main` และ compile ออก​มา​เป็น​โปรแกรม​ที่​รัน​ได้ package อื่น​นำ​เข้า​มัน​ไม่​ได้ คือ package ที่​แปล​ออก​มา​เป็น​โปรแกรม​ที่​รัน​ได้ · package อื่น​นำ​เข้า​มัน​ไม่​ได้ และ​มัน​ต้อง​มี​ฟังก์ชัน​ชื่อ main ที่​ไม่​รับ​อะไร​และ​ไม่​คืน​อะไร

$ go run ./cmd/orderctl
orderctl พร้อมทำงาน
$ go build ./cmd/orderctl
$ ./orderctl
orderctl พร้อมทำงาน

สังเกต​ว่า​เรา​สั่ง go run ./cmd/orderctl ไม่ใช่ go run main.go · Go ทำงาน​กับ package ไม่ใช่​กับ file เดี่ยว · การ​ระบุ file ตรงๆ ใช้ได้​ใน​กรณี​ง่าย​ที่สุด แต่​มัน​จะ​พัง​ทันที​ที่ package นั้น​มี​มากกว่า​หนึ่ง file

เป็น​ธรรมเนียม ไม่ใช่​ข้อ​บังคับ และ​มี​ข้อ​หนึ่ง​ที่​คน​เชื่อ​ผิด​กัน​มาก

orderctl/
go.mod
cmd/orderctl/main.go โปรแกรมบรรทัดคำสั่ง
cmd/orderd/main.go HTTP server ของบทที่ 8
internal/order/ ชนิดโดเมน ตรวจความถูกต้อง อ่าน JSON
internal/pricing/ ส่วนลดและใบสรุป
internal/store/ สถานะบน disk
internal/httpapi/ ตัวรับคำขอ
internal/batch/ ประมวลผลพร้อมกัน
testdata/ เครื่องมือของ Go ข้ามไดเรกทอรีนี้ให้เอง

internal/ เป็น​ชื่อ​ที่ Go เข้าใจ​จริง · package ที่​อยู่​ข้าง​ใต้​มัน ถูก import ได้​เฉพาะ​จาก ภายใน module เดียวกัน · นี่​คือ​กลไก​เดียว​ใน​ภาษา​ที่​บังคับ​ขอบเขต​การ​มอง​เห็น​ข้าม package

`pkg/` ไม่​ได้​อยู่​ใน​เอกสาร​ทางการ​เลย

คุณ​จะ​เจอ repository จำนวน​มาก​ที่​มี cmd/ pkg/ internal/ api/ ครบ​ชุด และ​เจอ บทความ​ที่​เรียก​มัน​ว่า “โครง​มาตรฐาน​ของ Go”

เอกสาร layout ทางการ​เสนอ เจ็ด​แบบ ไล่​จาก package เดียว​ไป​จนถึง server project · ไม่มี​แบบ​ไหน​เอ่ย​ถึง pkg/ เลย​สัก​ครั้ง

เอกสาร​เดียวกัน​เขียน​ถึง cmd/ ไว้​เต็ม​ประโยค​ว่า “while this isn’t strictly necessary in a repository that consists only of commands, it’s very useful in a mixed repository that has both commands and importable packages”

อ่าน​ครบ​ทั้ง​ประโยค​แล้ว​จะ​เห็น​ว่า​มัน​ไม่​ได้​ห้าม​ใช้ และ​ไม่​ได้​บังคับ​ให้​ใช้ · มัน​บอก​ว่า​มี​ประโยชน์ ตอน​ที่ repository มี​ทั้ง​โปรแกรม​และ package ที่​คน​อื่น​นำ​เข้า​ได้​ปน​กัน

คอร์ส​นี้​ใช้ cmd/ เพราะ​จะ​มี​สอง​โปรแกรม ไม่ใช่​เพราะ​กฎ​บังคับ

flowchart LR
  A["go.mod ที่ราก<br/>ประกาศ module path"] --> B["cmd/orderctl<br/>package main"]
  A --> C["internal/order<br/>ชนิดโดเมน"]
  A --> D["internal/pricing<br/>ส่วนลด"]
  B --> C
  B --> D
  E["module อื่นข้างนอก"] -. "import ไม่ได้" .-> C
  E -. "import ไม่ได้" .-> D

คำ​บรรยาย​ภาพ: go.mod ที่​ราก​ครอบ​ทุก​ไดเรกทอรี​ข้าง​ใต้ · package ใน internal ถูก​เรียก​ใช้ได้​จาก​ภายใน module เดียวกัน​เท่านั้น module ข้าง​นอก​เรียก​ไม่​ได้

สี่​คำ​สั่ง​นี้​ควร​อยู่​ใน​มือ​ก่อน​ไป​บท​หน้า

$ gofmt -l . # ไม่พิมพ์อะไรออกมา = ผ่าน
$ go vet ./... # ตรวจรูปแบบที่น่าสงสัย
$ go build ./... # ตรวจว่าแปลผ่าน แล้วทิ้งผลลัพธ์ทั้งหมด
$ go test ./... # ยังไม่มีเทสต์ จะได้ [no test files]

gofmt -l พิมพ์​เฉพาะ​ชื่อ file ที่​ยัง​จัด​รูป​แบบ​ไม่​ตรง · ถ้า​มัน​เงียบ​แปล​ว่า​เรียบร้อย · รูป​แบบ​นี้​ทำให้​เอา​มัน​ไป​วาง​ใน​เกต​ของ CI ได้​ตรงๆ

go build ./... ต่าง​จาก go build ./cmd/orderctl ตรง​ที่​มัน​ตรวจ​ทุก package แล้ว ทิ้ง​ผลลัพธ์​ทั้งหมด ไม่​เหลือ​โปรแกรม​ไว้​ให้ · ใช้​ตอน​อยาก​รู้​แค่​ว่า​แปล​ผ่าน​ไหม

บท​นี้​วาง​กรอบ​และ​ตั้ง​เครื่อง​มือ ยัง​ไม่มี feature ของ orderctl เกิด​ขึ้น​จริง

คุณ​รู้​แล้ว​ว่า Go เลือก​ไม่มี​อะไร​บ้าง และ​แลก​อะไร​ไป · คุณ​สร้าง module แรก อ่าน go.mod ออก​ทั้ง​สอง​บรรทัด และ​พิสูจน์​ด้วย​ตา​ตัว​เอง​ว่า​บรรทัด go เปลี่ยน​ผล​ของ​โปรแกรม​ได้​จริง

บท​หน้า​เรา​ลง​ของ​จริง — zero value, struct และ method · Go ไม่มี​สถานะ “ยัง​ไม่​กำหนด​ค่า” เลย ซึ่ง​เป็นการ​ตัดสิน​ใจ​ที่​ลบ bug ทั้ง​ตระกูล​หนึ่ง​ทิ้ง และ​สร้าง​ตระกูล​ใหม่​ขึ้น​มา​แทน


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

  • Go Modules Reference — go.mod file (เข้า​ถึง 2026-08-27) — บรรทัด go เป็น​ทั้ง​ขั้น​ต่ำ​ที่​ต้อง​มี และ​เป็น​กฎ​ภาษา​ที่ compiler บังคับ
  • Go 1.22 Release Notes (2024-02-06) — ตัว​แปร​ของ​ลูป​ถูก​สร้าง​ใหม่​ทุกร​อบ และ​เปลี่ยน​ตาม​บรรทัด go ของ​แต่ละ module
  • Organizing a Go module (เข้า​ถึง 2026-08-27) — โครง​เจ็ด​แบบ​ทางการ และ​ประโยค​ที่​ว่า cmd/ “isn’t strictly necessary”
  • Go Toolchains (เข้า​ถึง 2026-08-27) — toolchain ที่​รัน​จริง​กับ​บรรทัด go เป็น​คนละ​เรื่อง​กัน
  • cmd/go documentation (เข้า​ถึง 2026-08-27) — go build “compiles the packages but discards the resulting object”
  • Go Release History (2026-08-19) — Go 1.27.0 กับ​นโยบาย​ดูแล​ย้อน​หลัง​สอง​รุ่น​ใหญ่

เช็กความเข้าใจ — บทที่ 1

ข้อ 1 / 3

บรรทัด `go 1.25.0` ใน go.mod หมายความว่าอะไร