Go ตัดสินใจต่างตรงไหน และเครื่องมือชุดแรก
คุณเขียนภาษาอื่นเป็นอยู่แล้ว คอร์สนี้จึงไม่เริ่มที่ว่าตัวแปรคืออะไร
มันเริ่มที่คำถามที่มีประโยชน์กว่า — Go ตัดสินใจต่างจากภาษาที่คุณใช้อยู่ตรงไหน และมันจ่ายอะไรไปกับการตัดสินใจนั้น
คอร์สนี้สร้างของชิ้นเดียวไล่ยาวแปดบท — orderctl เครื่องมือบรรทัดคำสั่งที่ประมวลผล
ออเดอร์อาหาร อ่านจาก JSON ตรวจความถูกต้อง คิดส่วนลด เก็บสถานะ แล้วบทสุดท้ายเปิด HTTP
ใช้ standard library ล้วน ไม่มี framework ไม่มี ORM · บทนี้เป็นบทแนวคิดกับการตั้งค่า code ที่รันได้จริงมีแค่ hello world ของจริงเริ่มบทที่ 2
Java กับ Kotlin จะสร้างของชิ้นเดียวกันนี้ในคอร์สถัดไป เพื่อให้คุณเทียบสามภาษาได้ตรงๆ
สามข้อที่ Go เลือกไม่มี
หัวข้อที่มีชื่อว่า “สามข้อที่ Go เลือกไม่มี”Go ตัดสินใจ น้อยกว่า ภาษาส่วนใหญ่ และสามข้อนี้คือที่ที่คนย้ายมาสะดุดก่อนเสมอ
| สิ่งที่ภาษาของคุณมี | Go ให้แทน | ราคาที่จ่าย |
|---|---|---|
try/catch และ exception ที่พาตัวเองขึ้นไปหาผู้เรียก | error เป็นค่าธรรมดาที่ฟังก์ชันคืนกลับมา | คุณต้องเขียนว่าจะทำอะไรกับมันทุกจุดที่มันเกิด ไม่มีการกระโดดข้าม |
| การสืบทอด class และ virtual method | การฝังชนิดอื่นเข้าไป แล้วส่งต่อ method ให้ | ไม่มี override ไม่มี template method — ต้องออกแบบด้วยการประกอบแทน |
| ตัวเลือกของ formatter ที่ทีมต้องถกกัน | gofmt ที่ไม่มีตัวเลือกสไตล์ให้ตั้ง | คุณเลือกรูปแบบไม่ได้เลย แม้แต่ข้อที่คุณคิดว่าดีกว่า |
สามแถวนี้ไม่ใช่ข้อดีอย่างเดียว มันคือการแลก · Go ยอมเสียความยืดหยุ่นเพื่อให้ code ที่คนอื่นเขียน อ่านออกโดยไม่ต้องเรียนสไตล์ของทีมนั้นก่อน
ทุกบทของคอร์สนี้จะมีอย่างน้อยหนึ่งจุดที่บอกว่า สิ่งที่คุณคาดว่าจะเกิด กับสิ่งที่เกิดจริง ต่างกันตรงไหน เพราะจุดที่เจ็บที่สุดเวลาย้ายภาษา ไม่ใช่สิ่งที่ไม่รู้ แต่เป็นสิ่งที่คิดว่ารู้แล้ว
เครื่องมือคือ binary ตัวเดียว
หัวข้อที่มีชื่อว่า “เครื่องมือคือ binary ตัวเดียว”
goตัวเดียวทำงานที่ภาษาอื่นแบ่งให้เครื่องมือสามสี่ตัว
ติดตั้งจาก go.dev/dl แล้วตรวจว่าติดจริง
$ go versiongo version go1.27.0 darwin/arm64คำสั่งที่คุณจะใช้ทุกวันมีไม่กี่ตัว
go runแปลแล้วรันทันที ไม่ทิ้งผลลัพธ์ไว้go buildแปลเป็นโปรแกรมที่รันได้หนึ่งตัว ไม่ต้องมี runtime ติดไปด้วยgo testรันเทสต์ · ตัวรันเทสต์อยู่ในกล่องเดียวกัน ไม่ต้องลง library เพิ่มgo vetตรวจรูปแบบที่คอมไพล์ผ่านแต่แทบไม่มีทางเป็นสิ่งที่ตั้งใจgofmtจัดรูปแบบ · ไม่มีตัวเลือกสไตล์ให้ตั้ง
เทียบกับที่คุณรู้อยู่ ทุกแถวเป็นสะพาน ไม่ใช่ของเท่ากันเป๊ะ
| Go | ที่คุณคุ้น | จุดที่ต่าง |
|---|---|---|
go | dotnet CLI · Maven หรือ Gradle · npm scripts | ตัวเดียวคุมทั้ง build, test, dependency และการแจกจ่าย ไม่มี plugin ให้ตั้ง |
go build | dotnet publish · mvn package | ได้โปรแกรมเดียวที่รันบนเครื่องเปล่าได้ ไม่ต้องมี runtime หรือ JVM รออยู่ |
gofmt | Prettier · EditorConfig · dotnet-format | ไม่มีค่าให้ตั้ง จึงไม่มีอะไรให้ถกกันในรีวิว |
go.mod | .csproj · pom.xml · package.json | หนึ่ง file ต่อหนึ่ง module ไม่ใช่ต่อหนึ่ง project และไม่มีรายชื่อ file อยู่ข้างใน |
ถ้าคุณมาจาก C# หรือ Java คุณจะเผลอมองหาที่ที่ต้องลงทะเบียน file ใหม่
Go ไม่มีที่นั้น · หน่วยของการแปลคือ ไดเรกทอรี — ทุก file ในไดเรกทอรีเดียวกัน ที่ประกาศชื่อ package ตรงกัน คือ package เดียวกันโดยอัตโนมัติ
ผลที่ตามมาคือการเพิ่ม file ไม่ต้องแตะอะไรเลย แต่การวางของผิดไดเรกทอรีก็เงียบเหมือนกัน
module แรก
หัวข้อที่มีชื่อว่า “module แรก”
go mod initสร้างสิ่งเดียวที่คอร์สนี้ต้องการเพื่อเริ่มต้น
modulemoduleหน่วยการแจกจ่ายของ Go หนึ่ง module มี `go.mod` หนึ่ง file ที่ราก ประกาศ module path และรายการที่พึ่งพา — ไม่ใช่หนึ่ง file ต่อหนึ่ง package แบบ .csproj คือหน่วยการแจกจ่ายของ Go · หนึ่ง module มี go.mod หนึ่ง file ที่ราก
และครอบทุกไดเรกทอรีที่อยู่ข้างใต้
$ go mod init example.com/orderctlgo: creating new go.mod: module example.com/orderctl
$ cat go.modmodule example.com/orderctl
go 1.27.0สองบรรทัดนี้ทำงานคนละอย่างกันสิ้นเชิง
บรรทัด module คือชื่อที่คนอื่นใช้ import เข้ามา · มันไม่ต้องเป็นที่อยู่จริงบนอินเทอร์เน็ต
แต่ถ้าคุณจะเผยแพร่ มันควรตรงกับที่ที่ code อยู่จริง
บรรทัด go คือส่วนที่คนเข้าใจผิดกันมากที่สุด และหัวข้อถัดไปมีไว้เพื่อข้อนี้ข้อเดียว
บรรทัด go ไม่ใช่ป้ายบอกรุ่น
หัวข้อที่มีชื่อว่า “บรรทัด 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.21 | 3 3 3 | ตัวแปร i มีตัวเดียวตลอดทั้งลูป ฟังก์ชันทั้งสามจึงชี้ไปที่ตัวเดียวกัน |
go 1.25.0 | 0 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.0go: 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/orderctlorderctl พร้อมทำงาน
$ go build ./cmd/orderctl$ ./orderctlorderctl พร้อมทำงานสังเกตว่าเราสั่ง 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
คุณจะเจอ 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 หมายความว่าอะไร