บทเรียน 01 · Day 1 · Setup & Structure

กายวิภาคของโปรแกรม Go

~12 นาที · ต้องมี: ไม่ต้องมีอะไรเลย — เริ่มจากศูนย์ได้

เปิดไฟล์ main.go ครั้งแรกแล้วเห็นบรรทัดแปลก ๆ 5 บรรทัดข้างบน ที่ไม่รู้ว่ามีไว้ทำไม จบบทนี้คุณจะชี้ได้ทุกบรรทัดว่ามันคืออะไร และรันเรียงลำดับยังไง — ซึ่งเป็นสิ่งแรกที่ต้องรู้ก่อนอ่านโค้ดคนอื่นได้

ก่อนเริ่ม: ต้องมี Go ในเครื่องก่อน

เปิด 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 mainexecutable — build แล้วได้ไฟล์ที่ดับเบิลคลิกรันได้
package calculatorlibrary — เอาไว้ให้ package อื่น import ไปใช้ รันเองไม่ได้

import — ยืมของจาก standard library

import (
    "fmt"
    "math/rand"
)

กฎการตั้งชื่อ: ชื่อ package ที่เอามาใช้ = ชิ้นสุดท้ายของ import path → "math/rand" เรียกใช้ด้วย rand.Intn(10) ไม่ใช่ math.rand.Intn()

Go เข้มกว่าภาษาอื่น

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() มีได้หลายตัว และรันเรียงตามลำดับ ซึ่งเป็นเหตุผลที่มันไล่โค้ดยาก

ลำดับการทำงานทั้งหมดเป็นแบบนี้:

1. import package ทั้งหมด (ถ้า package นั้นมี init ก็รันของมันก่อน) 2. ประกาศตัวแปรระดับ package (var courseName = "...") 3. รัน init() ← มีได้หลายตัว รันตามลำดับที่เขียน 4. รัน main() ← โปรแกรมเริ่มจริงตรงนี้
ใช้ 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-to-hero/ ├── go.mod ← module path + Go version + dependencies ├── go.sum ← ลายนิ้วมือ (checksum) ของ dependency แต่ละตัว ใช้เช็คว่าของที่โหลดมาเป็นตัวเดิมจริง └── main.go ← โค้ดของเรา
คำสั่งทำอะไร
go mod init <ชื่อ>สร้าง go.mod ครั้งแรก
go mod tidyเพิ่มของที่ใช้ ลบของที่ไม่ใช้ — รันบ่อย ๆ
go run .compile ใส่ที่ชั่วคราว → รัน → ลบทิ้ง
go buildcompile เป็นไฟล์ 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 ───────────────────── ───────────────────── main.go source + dependencies ↓ ↓ load package Go compiler ↓ ↓ compile → binary ชั่วคราว Linker ↓ ↓ รัน binary binary จริง เก็บไว้บนดิสก์ ↓ ลบ binary ทิ้ง

ใช้ 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 · ลงมือทำ

ภารกิจ: สร้างโปรเจกต์แรกด้วยมือ

  1. สร้างโฟลเดอร์ใหม่ แล้วรัน go mod init hello — เปิด go.mod ดู ต้องเห็น 2 บรรทัดคือ module hello และ go 1.xx (เลขเวอร์ชัน Go ในเครื่องคุณ) ถ้าเห็นแบบนี้ถือว่าผ่าน ไปข้อต่อไปได้
  2. เขียน main.go ที่พิมพ์ชื่อตัวเองออกมา แล้ว go run .
  3. เพิ่ม func init() ที่พิมพ์ "เตรียมของ..." — รันดูว่าลำดับเป็นอย่างที่คาดไหม
  4. เพิ่ม init() ตัวที่สอง (Go ยอมให้มีหลายตัว) — ทำนายก่อนว่าจะรันเรียงยังไง แล้วค่อยรันพิสูจน์
  5. ลอง import "os" ไว้เฉย ๆ ไม่ใช้ — อ่าน error ให้ครบ
  6. ท้าทาย: 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 ได้เลย เรื่องนี้ต้องผ่านให้ได้ก่อนทำบทอื่น

อ้างอิง