เปิด terminal พิมพ์ go version — ถ้าขึ้นเลขเวอร์ชัน เช่น
go version go1.26.5 darwin/arm64 แปลว่าพร้อมแล้ว ข้ามไปหัวข้อ 1 ได้เลย
ถ้าขึ้น command not found: go ให้ติดตั้งก่อน — โหลดตัวติดตั้งจาก
go.dev/dl หรือถ้าใช้ mise (เครื่องมือจัดการเวอร์ชันภาษา) อยู่แล้ว
สั่ง mise use -g go@latest จากนั้นปิด terminal เปิดใหม่
แล้วเช็ค go version อีกครั้ง
แนะนำให้ลง VS Code แล้วติดตั้ง extension ชื่อ "Go" (ของ Go Team) ด้วย จะได้จัดรูปแบบโค้ดอัตโนมัติตอนกด save ตามที่จะพูดถึงในหัวข้อ 5
1 · โปรแกรมที่เล็กที่สุดที่รันได้
package main // ← 1. อยู่ package ไหน
import "fmt" // ← 2. ยืมของจากใครมาใช้
func main() { // ← 3. จุดเริ่มต้นของโปรแกรม
fmt.Println("Hello Go")
}
สามบรรทัดนี้ ขาดตัวใดตัวหนึ่งไม่ได้ มาดูทีละอัน
ลองนึกถึงกล่องเครื่องมือในบ้าน เครื่องมือที่อยู่กล่องเดียวกัน
หยิบมาใช้ด้วยกันได้เลย ไม่ต้องขออนุญาตใคร
แต่ถ้าอยากได้สว่านที่อยู่กล่องของเพื่อนบ้าน ต้องเดินไปยืมกล่องนั้นมาวางข้าง ๆ ก่อน
Go ก็คิดแบบเดียวกันเป๊ะ ๆ:
· package main = บอกว่าไฟล์นี้อยู่กล่องไหน
· import "fmt" = ยืมกล่องของคนอื่นมาวางข้าง ๆ
· func main() = ปุ่มเปิดเครื่อง — กดแล้วงานเริ่มตรงนี้
package main — คำที่มีความหมายพิเศษ
ก่อนอื่นต้องรู้จักคำว่า package ก่อน — package คือ "กล่องเก็บโค้ด" หนึ่งกล่อง
ไฟล์ .go ที่อยู่โฟลเดอร์เดียวกันและประกาศชื่อ package เดียวกัน ถือว่าอยู่กล่องเดียวกัน
โค้ดในกล่องเดียวกันเรียกหากันได้ตรง ๆ ส่วนของจากกล่องอื่นต้อง import เข้ามาก่อนถึงจะใช้ได้
ทุกไฟล์ Go ต้องบอกว่าตัวเองอยู่กล่องไหน โดยเขียนไว้บรรทัดบนสุด แต่ชื่อ main
พิเศษกว่าชื่ออื่น: เป็นชื่อเดียวที่บอก compiler (โปรแกรมที่แปลโค้ดของเราให้เป็นไฟล์ที่เครื่องรันได้)
ว่า "อันนี้เป็นโปรแกรมที่รันได้"
| เขียน | ได้อะไร |
|---|---|
package main | executable — build แล้วได้ไฟล์ที่ดับเบิลคลิกรันได้ |
package calculator | library — เอาไว้ให้ package อื่น import ไปใช้ รันเองไม่ได้ |
import — ยืมของจาก standard library
import (
"fmt"
"math/rand"
)
กฎการตั้งชื่อ: ชื่อ package ที่เอามาใช้ = ชิ้นสุดท้ายของ import path
→ "math/rand" เรียกใช้ด้วย rand.Intn(10) ไม่ใช่ math.rand.Intn()
import มาแล้วไม่ใช้ = compile ไม่ผ่าน (ไม่ใช่แค่ warning) เหมือนกับตัวแปรที่ประกาศแล้วไม่ใช้ — Go บังคับให้โค้ดสะอาดตั้งแต่ compile time ตอนแรกจะรำคาญ แต่แปลว่า dependency ในโปรเจกต์จะไม่มีขยะสะสม
func main() — ประตูทางเข้า
ต้องชื่อ main เป๊ะ · ต้องอยู่ใน package main ·
ห้ามรับ parameter และห้ามคืนค่า · มีได้ตัวเดียวทั้งโปรแกรม
ตรงนี้สำคัญ: เวลาไปถาม dev คนอื่นหรือถาม AI ถ้าใช้คำให้ตรง จะได้คำตอบที่ตรง คำพวกนี้คือคำที่เขาใช้เรียกสิ่งที่คุณเพิ่งเข้าใจไป
- package
- “กล่องเครื่องมือ” หนึ่งกล่อง — พูดว่า “โค้ดนี้อยู่ package ไหน” แทนที่จะพูดว่า “อยู่โฟลเดอร์ไหน”
- entry point
- จุดที่โปรแกรมเริ่มทำงาน = “ปุ่มเปิดเครื่อง” ของเรา · ในภาษา Go คือ
func main() - executable / binary
- ไฟล์ที่กดรันได้เลย — ได้จาก
package mainเท่านั้น - library
- กล่องที่มีไว้ให้คนอื่นยืม รันเองไม่ได้ — คือ package ที่ไม่ได้ชื่อ
main - standard library
- กล่องเครื่องมือที่ติดมากับ Go อยู่แล้ว เช่น
fmt· ไม่ต้องโหลดเพิ่ม - compiler
- ตัวแปลโค้ดของเราให้เป็นไฟล์ที่เครื่องรันได้ · ประโยคที่จะได้ยินบ่อยคือ “compile ไม่ผ่าน” = แปลไม่สำเร็จ ยังไม่ทันได้รันเลย
2 · init() — ฟังก์ชันที่รันก่อน main
package main
import "fmt"
func init() {
fmt.Println("Welcome to init function")
}
func main() {
fmt.Println("Hello Go")
}
โค้ดนี้พิมพ์อะไรออกมาก่อน?
Welcome to init function
Hello Go
init() รันก่อนเสมอ — มันคือที่สำหรับ "เตรียมของ" ก่อนโปรแกรมเริ่มจริง
และคุณเรียก init() เองไม่ได้ runtime เรียกให้อัตโนมัติ
ร้านกาแฟไม่ได้เปิดประตูรับลูกค้าทันทีที่พนักงานมาถึง
ก่อนเปิดร้านมี “ช่วงเตรียม” — ต้มน้ำ เปิดเครื่องชง เช็ดโต๊ะ นับเงินทอน
ลูกค้ายังเข้าไม่ได้ แต่ถ้าไม่มีช่วงนี้ พอเปิดร้านจริงจะชงกาแฟไม่ได้เลย
init() คือช่วงเตรียมก่อนเปิดร้าน ·
main() คือตอนเปิดประตูรับลูกค้า
และสังเกตว่าไม่มีใครสั่งให้พนักงานมาเตรียมของ — มันเป็นตารางที่ตกลงกันไว้แล้ว
เหมือนที่เราเรียก init() เองไม่ได้ Go เรียกให้อัตโนมัติ
ที่ไม่เหมือนกัน: ร้านกาแฟมีช่วงเตรียมช่วงเดียว แต่ init()
มีได้หลายตัว และรันเรียงตามลำดับ ซึ่งเป็นเหตุผลที่มันไล่โค้ดยาก
ลำดับการทำงานทั้งหมดเป็นแบบนี้:
ใช้เท่าที่จำเป็นจริง ๆ (ลงทะเบียน driver, ตั้งค่า config ที่คำนวณไม่ได้ตอนประกาศ)
เพราะมันรันเงียบ ๆ โดยไม่มีใครเรียก ทำให้ไล่โค้ดยากและเทสต์ยาก
ถ้าเขียนเป็นฟังก์ชันธรรมดาแล้วเรียกจาก main ได้ ให้ทำแบบนั้นดีกว่า
- runtime
- “ตัวคุมงาน” ที่ Go แถมมากับโปรแกรมทุกตัว · มันคือคนที่เรียก
init()และmain()ให้เรา · เวลาได้ยินคำว่า “พังตอน runtime” แปลว่า พังตอนโปรแกรมกำลังทำงานอยู่ ไม่ใช่ตอน compile - initialization
- ช่วง “เตรียมของก่อนเริ่ม” ·
init()มาจากคำนี้ - side effect
- ผลข้างเคียงที่เกิดขึ้นเองโดยไม่มีใครเรียก ·
init()เป็นตัวอย่างคลาสสิก และเป็นเหตุผลที่ dev หลายคนบอกว่า “เลี่ยง init ได้ให้เลี่ยง”
3 · Module — บ้านของโปรเจกต์
ก่อนเขียนโค้ดต้องมี module ก่อน — module คือ "1 โปรเจกต์" ในสายตาของ Go
คือกลุ่มของ package ทั้งหมดที่อยู่ใต้โฟลเดอร์เดียวกัน ถูกแจกจ่ายและระบุเวอร์ชันไปพร้อมกันทั้งชุด
เทียบง่าย ๆ: package = กล่องใบหนึ่ง, module = ลังที่ใส่กล่องหลายใบไว้ด้วยกัน
โดยมีไฟล์ go.mod ที่โฟลเดอร์บนสุดเป็นบัตรประชาชน บอกชื่อโปรเจกต์ เวอร์ชัน Go ที่ใช้
และรายชื่อ library ภายนอกที่ยืมมาใช้ (เรียกว่า dependency)
· ห้องในบ้าน = package (กล่องเครื่องมือจากหัวข้อ 1)
· บ้านทั้งหลัง = module — รวมทุกห้องไว้ด้วยกัน ย้ายบ้านทีก็ย้ายไปทั้งหลัง
· ทะเบียนบ้าน = go.mod — เอกสารแผ่นเดียวที่บอกว่า
บ้านนี้ชื่ออะไร สร้างด้วย Go รุ่นไหน และยืมของจากบ้านไหนมาบ้าง
ส่วน go.sum เหมือนใบเสร็จที่มีลายเซ็นกำกับ —
คราวหน้าโหลดของชิ้นเดิมมา จะเทียบลายเซ็นได้ว่าเป็นของชิ้นเดิมจริง ไม่มีใครสลับไส้ใน
mkdir go-to-hero
cd go-to-hero
go mod init go-to-hero
| คำสั่ง | ทำอะไร |
|---|---|
go mod init <ชื่อ> | สร้าง go.mod ครั้งแรก |
go mod tidy | เพิ่มของที่ใช้ ลบของที่ไม่ใช้ — รันบ่อย ๆ |
go run . | compile ใส่ที่ชั่วคราว → รัน → ลบทิ้ง |
go build | compile เป็นไฟล์ binary จริงเก็บไว้ |
- module
- “บ้านทั้งหลัง” = 1 โปรเจกต์ในสายตา Go · เวลาถามคนอื่นให้พูดว่า “module path ของโปรเจกต์คืออะไร” จะสื่อกว่าถามว่า “ชื่อโปรเจกต์อะไร”
- module path
- ชื่อเต็มของบ้าน ที่เขียนบรรทัดแรกใน
go.mod· โปรเจกต์จริงมักตั้งเป็น URL ของ repo เช่นgithub.com/user/wongnokเพราะคนอื่นต้องใช้ชื่อนี้ import ของเรา - dependency
- “ของที่ยืมมาจากบ้านอื่น” — library ภายนอกที่โปรเจกต์เราเรียกใช้
- checksum
- “ลายเซ็นกำกับ” ใน
go.sum· ใช้ยืนยันว่าไฟล์ที่โหลดมาไม่ถูกแก้ระหว่างทาง
4 · go run ต่างจาก go build ยังไง
อ่านจากบนลงล่างทีละฝั่ง ไม่ต้องอ่านเทียบบรรทัดต่อบรรทัด — ทั้งสองฝั่งเริ่มจากโค้ดของเราเหมือนกัน
ต่างกันที่ตอนจบว่าเก็บผลลัพธ์ไว้หรือลบทิ้ง
binary = ไฟล์ที่เครื่องรันได้โดยตรง (ไม่ต้องมีโค้ดต้นฉบับแล้ว) ·
Linker = ขั้นตอนที่เอาโค้ดที่ compile เสร็จทุกชิ้น รวมทั้งของที่ import มา มาต่อรวมเป็นไฟล์เดียว
ใช้ go run ตอนพัฒนา (ไม่มีไฟล์ขยะ) ·
ใช้ go build ตอนจะเอาไป deploy
จุดที่ทำให้ Go ดังในโลก DevOps คือ build ข้ามเครื่องได้จากเครื่องเดียว — สั่งจาก MacBook ให้ได้ binary สำหรับ Linux server ได้เลย:
GOOS=linux GOARCH=amd64 go build -o bin/app
GOOS = ระบบปฏิบัติการปลายทาง (linux, windows, darwin = macOS) ·
GOARCH = สถาปัตยกรรม CPU ปลายทาง (amd64 คือ Intel/AMD, arm64 คือ Apple Silicon และ server รุ่นใหม่) ·
-o bin/app = บอกว่าให้เอาไฟล์ผลลัพธ์ไปวางไว้ที่ bin/app
บน Windows PowerShell ตั้งค่าแบบวางไว้หน้าคำสั่งแบบนี้ไม่ได้ ให้เขียนเป็น
$env:GOOS="linux"; $env:GOARCH="amd64"; go build -o bin/app แทน
ได้ไฟล์เดียวจบ ไม่ต้องลง runtime อะไรที่ปลายทาง — นี่คือเหตุผลที่ Docker, Kubernetes, Terraform, Prometheus, ArgoCD เขียนด้วย Go กันหมด
5 · เครื่องมือ 2 ตัวที่ต้องรู้จักตั้งแต่วันแรก
go fmt (Formatting) | go vet (Linting) | |
|---|---|---|
| ทำอะไร | จัดรูปแบบโค้ดให้เป็นมาตรฐาน | หาโค้ดที่ compile ผ่านแต่น่าจะผิด |
| ตัวอย่าง | เว้นวรรค ย่อหน้า เรียง import | สั่งพิมพ์ตัวเลขด้วยรูปแบบของข้อความ เช่น fmt.Printf("%s", 42) — compile ผ่าน แต่ผลลัพธ์ออกมาเพี้ยนแน่นอน ตอน run (ได้ %!s(int=42) แทนที่จะเป็น 42) |
| ผลลัพธ์ | Go ทุกคนในโลกเขียนหน้าตาเหมือนกัน | จับบั๊กก่อนถึงมือ reviewer |
ในภาษาอื่นทีมต้องเถียงกันเรื่อง tab หรือ space — Go ตัดจบให้ตั้งแต่แรก
VS Code ที่ลง Go extension แล้วจะ go fmt ให้อัตโนมัติทุกครั้งที่กด save
6 · ทดสอบความจำ
1. ไฟล์เขียนว่า package calculator แล้ว go build จะได้อะไร?
เฉพาะ package main เท่านั้นที่ build ออกมาเป็น executable
ส่วน package อื่นเป็น library ที่ compile ผ่านได้ปกติแต่ไม่มีจุดเริ่มต้นให้รัน
2. โปรแกรมมี init() และ main() — อะไรทำงานก่อน?
init() รันก่อนเสมอ ไม่ว่าจะเขียนไว้ตรงไหนของไฟล์
และเรียกเองไม่ได้ — runtime เรียกให้อัตโนมัติ
3. import "os" ไว้แต่ไม่ได้ใช้เลย จะเกิดอะไรขึ้น?
"os" imported and not used — Go ถือว่าเป็น error
ไม่ใช่ warning เช่นเดียวกับตัวแปรที่ประกาศแล้วไม่ใช้
ถ้าอยาก import เพื่อให้ init() ของมันทำงานอย่างเดียว
ใช้ import _ "package" (blank identifier)
4. อยาก build บน Mac ให้ได้ไฟล์ที่รันบน Linux server ต้องทำยังไง?
Cross-compilation เป็นความสามารถในตัวของ Go ไม่ต้องลง toolchain
เพิ่ม — GOOS คือระบบปฏิบัติการ, GOARCH คือสถาปัตยกรรม CPU
(amd64, arm64, …)
7 · ลงมือทำ
ภารกิจ: สร้างโปรเจกต์แรกด้วยมือ
- สร้างโฟลเดอร์ใหม่ แล้วรัน
go mod init hello— เปิดgo.modดู ต้องเห็น 2 บรรทัดคือmodule helloและgo 1.xx(เลขเวอร์ชัน Go ในเครื่องคุณ) ถ้าเห็นแบบนี้ถือว่าผ่าน ไปข้อต่อไปได้ - เขียน
main.goที่พิมพ์ชื่อตัวเองออกมา แล้วgo run . - เพิ่ม
func init()ที่พิมพ์"เตรียมของ..."— รันดูว่าลำดับเป็นอย่างที่คาดไหม - เพิ่ม
init()ตัวที่สอง (Go ยอมให้มีหลายตัว) — ทำนายก่อนว่าจะรันเรียงยังไง แล้วค่อยรันพิสูจน์ - ลอง
import "os"ไว้เฉย ๆ ไม่ใช้ — อ่าน error ให้ครบ - ท้าทาย:
GOOS=linux GOARCH=arm64 go build -o bin/appแล้วรันfile bin/app— ถือว่าผ่านเมื่อผลลัพธ์มีคำว่าELF 64-bitและARM aarch64(ถ้าเป็น binary ของ Mac จะขึ้นMach-Oแทน) · บน Windows ไม่มีคำสั่งfileให้ดูแค่ว่าไฟล์bin\appถูกสร้างขึ้นมาจริงก็พอ
เปิดไฟล์ Go ของใครก็ตาม คุณอ่าน 5 บรรทัดบนสุดออกทันทีว่า อยู่ package ไหน ยืมอะไรมา และเริ่มรันตรงไหน
อ่านต่อจากต้นทาง
แหล่งหลักที่แนะนำ:
Tutorial: Get started with Go —
ทางการ สั้น ทำตามได้ใน 10 นาที ครอบคลุมตั้งแต่ go mod init ถึงรันโปรแกรมแรก
💬 ติดตรงไหนถามได้เลย — โดยเฉพาะเรื่องติดตั้ง (mise / VS Code extension) ถ้าติดตั้งไม่ผ่านบอกมาพร้อม error ได้เลย เรื่องนี้ต้องผ่านให้ได้ก่อนทำบทอื่น
อ้างอิง
- Get started with Go — module, go mod init, โปรแกรมแรก
- Go spec — Program initialization and execution — ลำดับ package var → init() → main()
- How to Write Go Code — โครงสร้าง module และ package
- Go Code Review Comments — มาตรฐานที่
go fmt/go vetบังคับใช้