ขึ้นชั้นกลาง: module, tools, slice/map, generics, context
ลองตอบในใจ ถ้าทำได้เกิน 7 ข้อขึ้นเล่มนี้ได้เลย ถ้ายังไม่ได้กลับไปเปิดเล่ม Beginner ตอนนั้นก่อน:
go mod init + รัน go run . ได้:= ต่างจาก = ยังไงif err != nil ได้โดยไม่ต้องดูfor + range วน slice/map ได้append โดยไม่ลืม s = append(s, ...)p.Name ได้go func + close + range ได้เช็กเวอร์ชันตัวเองก่อน:
เล่มก่อนเราสร้างโฟลเดอร์ hello แล้ว go mod init hello ได้ไฟล์ go.mod มา 1 ไฟล์
ให้นึกว่า go.mod = บัตรประชาชนของโปรเจกต์ บอกว่า:
เปิดดู:
จะเห็นประมาณนี้ (Go 1.27):
หลายคนงง: "ลง 1.27 ทำไมขึ้น 1.25?" ตอบ: ปกติ Go ตั้งใจให้เลขต่ำไว้ก่อนเพื่อให้เพื่อนที่ยังไม่อัปเดตเปิดได้ ไม่ต้องแก้ ถ้าอยากขยับค่อยสั่ง:
แล้วใช้ได้เลย:
Go จะจดลง go.mod + สร้าง go.sum (ใบเสร็จกันของปลอม) ให้เอง จบด้วย:
ไม่ต้องซับซ้อน เริ่มแบบนี้พอ:
กฎง่ายๆ:
store ไม่ใช่ StoreData4 คำสั่งนี้คือชุดปฐมพยาบาล ใช้ทุกวัน:
go fmt ./... # แม่บ้าน: จัดหน้าให้สวย
go vet ./... # หมอ: ตรวจว่าสั่งอะไรแปลกๆ ไหม
go fix ./... # ครู: (ของใหม่ Go 1.26 เก่งมาก) แก้โค้ดเก่าเป็นแบบใหม่ให้เอง
go test ./... # ครูสอบ: รันเทสต์go fix ของใหม่ เก่งขึ้นเยอะGo 1.26 รื้อ go fix ใหม่หมด มีตัวช่วยชื่อ modernizers เช่น:
atomictypes — เปลี่ยนวิธีเขียน atomic เก่าเป็นแบบใหม่embedlit — ช่วยเรื่อง struct ฝังกัน (ตรงกับบทที่ 4)slicesbackward — ช่วยวน slice กลับหลังให้ถูกunsafefuncs — เตือนจุดที่ใช้ unsafe เสี่ยงพังวิธีใช้แบบคนขี้เกียจ:
มันจะบอกว่าจะแก้อะไรบ้าง ให้กดยืนยัน ไม่ต้องจำเองทุกตัว
จำภาพนี้: slice = กระดาษโน้ตแปะไว้ว่า ของอยู่ตรงไหน ยาวเท่าไหร่ จุได้เท่าไหร่
len = ของที่มีตอนนี้cap = จุได้สูงสุดก่อนต้องย้ายบ้านกับดักยอดฮิต:
a := []int{1, 2, 3}
b := a // b ชี้บ้านหลังเดียวกัน!
b[0] = 99
fmt.Println(a) // [99 2 3] a โดนด้วย!ถ้าอยากได้บ้านใหม่จริงๆ ต้อง copy หรือ append ไป slice ว่าง:
และห้ามลืม:
ข่าวดี Go 1.26: compiler ฉลาดขึ้น เอาที่เก็บ slice ไปไว้บน stack ได้บ่อยขึ้น = เร็วขึ้นเองโดยไม่ต้องแก้โค้ด
Map เหมือนสมุดโทรศัพท์เล่มเดียว ถ้า 2 คนเขียนพร้อมกันจะขาด:
ทางแก้มี 2 แบบ:
// แบบ 1: ใส่กุญแจ (ง่ายสุด)
var mu sync.Mutex
mu.Lock()
m[key] = v
mu.Unlock()
// แบบ 2: ใช้ sync.Map (เหมาะกับอ่านเยอะเขียนน้อย)
var sm sync.Map
sm.Store(key, value)
v, ok := sm.Load(key)เช็กว่ามี key ไหมยังใช้สูตรเดิม:
type Logger struct{}
func (Logger) Log(s string) { fmt.Println("log:", s) }
type Job struct {
Command string
Logger // ฝังไว้ ไม่ต้องตั้งชื่อ field
}
j := Job{Command: "run"}
j.Log("start") // เรียกของ Logger ได้เลยเหมือนเป็นของตัวเองของใหม่ Go 1.27: ตอนสร้างก็ยัดไส้ชั้นในได้เลย ไม่ต้องแกะ:
type Habitat struct{ Burrow string }
type Gopher struct {
Name string
Habitat
}
g := Gopher{
Name: "Gopher",
Burrow: "Burrow #42", // เมื่อก่อนต้องเขียน Habitat: Habitat{Burrow: ...} ยาวกว่า
}go fix มีตัว embedlit คอยช่วยเปลี่ยนแบบเก่าเป็นแบบใหม่ให้ด้วย
new แบบใหม่ สั้นลงเล่มก่อนบอกว่า new(T) = สร้างกล่องเปล่า ตอนนี้ Go 1.26 ใส่ไส้ได้เลย:
// เก่า: 2 บรรทัด
x := int64(300)
p1 := &x
// ใหม่: บรรทัดเดียว ความหมายเดียวกัน
p2 := new(int64(300))อ่านว่า "สร้างกล่อง int64 ใส่ 300 พร้อมบอกที่อยู่" ใช้ตอนอยากได้ pointer แบบไวๆ
func counter() func() int {
n := 0
return func() int { // ฟังก์ชันข้างในจำ n ได้แม้ข้างนอกจบไปแล้ว
n++
return n
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2เหมือนกระปุกออมสินที่จำได้ว่าหยอดไปเท่าไหร่แล้ว
...func Sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
Sum(1, 2) // 3
Sum(1, 2, 3, 4) // 10
s := []int{1, 2, 3}
Sum(s...) // มี ... ท้าย = แกะกล่องโปรยเข้าไปรู้แล้วว่า defer = สัญญาว่าจะทำตอนจบ แต่มี 2 ทริก:
func work() {
mu.Lock()
defer mu.Unlock() // ปลดล็อกแน่ ไม่ว่าจะ return ทางไหน
f, err := os.Open("a.txt")
if err != nil { return }
defer f.Close() // ปิดไฟล์แน่
}อาร์กิวเมนต์ถูกถ่ายรูปตอน defer ไม่ใช่ตอนทำจริง:
for i := 0; i < 3; i++ {
defer fmt.Println(i) // เก็บค่า i ตอนนั้นเลย
}
// พิมพ์ 2 1 0 (เข้าหลังออกก่อน)ใครมีเมธอด String() string ถือว่าเป็นพวกเดียวกันอัตโนมัติ ไม่ต้องสมัคร:
กฎ: รับ interface เล็กๆ คืนของจริงก้อนใหญ่ โค้ดจะต่อกับใครก็ได้
เมื่อก่อนอยากได้ฟังก์ชันใช้ได้หลาย type ต้องเขียนซ้ำ ตอนนี้เขียนทีเดียว:
func Min[T int | float64](a, b T) T {
if a < b { return a }
return b
}
Min(1, 2) // int
Min(1.5, 2.5) // float64ของใหม่ Go 1.27 มี 2 อย่าง:
1. Generic method ใน struct ได้แล้ว (เมื่อก่อนทำได้แค่ func ลอยๆ):
// ตัวอย่างจาก math/rand/v2
func (r *Rand) N[Int intType](n Int) Int
// เมธอดเดียวใช้ได้ทุกชนิด int ไม่ต้องเขียน Int32N Int64N แยกกันแล้ว2. Go เดาชนิดให้ในที่ที่เคยต้องเขียนย้ำ รวมถึงส่งเข้า channel:
func GenericFormatter[T any](v T) string {
return fmt.Sprintf("value: %v", v)
}
type IntFormatter func(int) string
formatters := []IntFormatter{GenericFormatter} // ไม่ต้อง [int] แล้ว เดาให้
ch := make(chan IntFormatter, 1)
ch <- GenericFormatter // ส่งเข้า channel ได้เลย เดาให้เหมือนกันมือกลางจำแค่นี้: ถ้าเห็นโค้ด generic ไม่มี [ ] แล้วไม่พัง นั่นคือของใหม่ 1.27 ไม่ใช่บั๊ก
เล่มก่อน: if err != nil เช็กเลย เล่มนี้: ทำให้ error บอกทางกลับบ้านได้
%wdata, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("อ่าน config: %w", err) // %w = ห่ออันเก่าไว้ข้างใน แกะดูได้
}if errors.Is(err, os.ErrNotExist) {
// ไฟล์ไม่มีจริงๆ ไม่ใช่พังแบบอื่น
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("พังที่ path:", pathErr.Path)
}เหมือนซองจดหมายซ้อนกัน Is = ถามว่าไส้ในใช่คนนี้ไหม As = แกะเอาคนข้างในออกมา
type AgeError struct{ Age int }
func (e *AgeError) Error() string {
return fmt.Sprintf("อายุ %d ใช้ไม่ได้", e.Age)
}อย่า panic กับงานปกติ ให้ return error ส่วน panic + recover เก็บไว้กันตายทั้งเซิร์ฟเวอร์เท่านั้น
ทบทวนแบบ 1 นาที:
go f() = จ้างพ่อครัวเพิ่มch <- v = วางจาน, <-ch = หยิบจานclose(ch) = บอกว่าหมดแล้ว คนส่งเท่านั้นที่ปิดfor v := range ch = หยิบจนหมดunbuf := make(chan int) // ส่งแล้วต้องมีคนรับทันที (ซิงค์ 100%)
buf := make(chan int, 10) // วางค้างได้ 10 ชิ้น (กันพ่อครัวรอ)select {
case v := <-ch:
fmt.Println(v)
case <-time.After(2 * time.Second):
fmt.Println("รอ 2 วิแล้ว ไม่มา เลิก!")
}ทริก non-blocking:
ของไหลเป็นทอดๆ:
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums { out <- n }
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in { out <- n * n }
close(out)
}()
return out
}
// ใช้: for n := range sq(gen(1,2,3,4)) { ... } // 1 4 9 16ถ้างานหนักให้แตกหลายคนทำแล้วรวม (fan-out/fan-in) + worker pool แบบเล่มก่อน
กับดัก:
all goroutines are asleep - deadlock = ส่งแล้วไม่มีคนรับ หรือรับแล้วไม่มีคนส่งsend on closed channel = ส่งหลัง closeตรวจเสมอ:
พ่อครัวทำอยู่ ลูกค้ายกเลิก จะตะโกนบอกยังไง? ใช้ context:
func work(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("เสร็จ")
case <-ctx.Done():
fmt.Println("โดนยกเลิก:", ctx.Err())
}
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go work(ctx)WithTimeout = ให้เวลาทำ 2 วิWithCancel = ทำจนกว่าจะสั่งหยุดเองcancel มาแล้วต้อง defer cancel() เสมอ ไม่งั้นรั่วvar mu sync.Mutex
mu.Lock()
// แตะของที่ใช้ร่วมกันตรงนี้
mu.Unlock()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// ทำงาน
}()
wg.Wait() // รอให้หมด
var once sync.Once
once.Do(func() { /* ทำแค่ครั้งเดียว เช่น init */ })ของใหม่ Go 1.26 มี go fix atomictypes ช่วยเปลี่ยนโค้ด atomic เก่าเป็นแบบใหม่ให้ ถ้าเจอโค้ด sync/atomic แปลกๆ ให้รัน go fix ดู
func TestAdd(t *testing.T) {
tests := []struct{ a, b, want int }{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tt := range tests {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("Add(%d,%d)=%d want %d", tt.a, tt.b, got, tt.want)
}
}
}ของใหม่: testing/synctest + httptest.NewTestServer ไว้เทสต์โค้ด concurrent + HTTP โดยไม่ต้องต่อเน็ตจริง (เซิร์ฟเวอร์ปลอมใน memory) — ถ้าเพิ่งขึ้นกลางรู้ว่ามีก็พอ ยังไม่ต้องใช้ก็ได้
Go 1.27 มี encoding/json/v2 (แบบตั้งค่าได้) + jsontext (แบบสตรีม) และเอาไส้ v2 มาใส่ใน encoding/json ตัวเดิมให้เลย = โค้ดเดิมเร็วขึ้นเองโดยไม่ต้องแก้
มือกลางจำแค่นี้:
json.Marshal / Unmarshal ยังใช้ได้json/v2 ทีหลังtype User struct {
Name string `json:"name"`
Age int `json:"age"`
}
b, _ := json.Marshal(User{"Ann", 20})
fmt.Println(string(b)) // {"name":"Ann","age":20}ถ้าโปรแกรมช้า อย่าเพิ่งโทษภาษา ให้เช็ก 3 อย่างก่อน:
close channel ไหม → goroutine ค้างโจทย์: ดึงราคา 5 หุ้นพร้อมกัน ใครเกิน 500ms ถือว่าตก แล้วมีเทสต์
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"time"
)
func fetchPrice(ctx context.Context, s string) (int, error) {
select {
case <-time.After(time.Duration(rand.Intn(800)) * time.Millisecond):
return rand.Intn(1000), nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
func main() {
symbols := []string{"AAPL", "GOOG", "TSLA", "AMZN", "MSFT"}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
var wg sync.WaitGroup
ch := make(chan string, len(symbols))
for _, s := range symbols {
wg.Add(1)
go func(sym string) {
defer wg.Done()
price, err := fetchPrice(ctx, sym)
if err != nil {
ch <- fmt.Sprintf("%s: ตก (%v)", sym, err)
return
}
ch <- fmt.Sprintf("%s = %d", sym, price)
}(s)
}
go func() { wg.Wait(); close(ch) }()
for r := range ch {
fmt.Println(r)
}
}แบบฝึกต่อ:
go vet + go fix ดูว่ามันแนะนำอะไรfetchPrice ด้วย context 10ms ต้อง errorjson.Marshalgo test -race + เปลี่ยน buffer channel เป็น 0 แล้วดูว่าช้าลงไหมไปต่อ:
| คำ | แปลบ้านๆ |
|---|---|
| module | บัตรประชาชนโปรเจกต์ (ไฟล์ go.mod) |
| dependency | ของที่ยืมคนอื่นมาใช้ |
| closure | ฟังก์ชันจำบ้านเกิดได้ |
| generic | สูตรครอบจักรวาล ใช้ได้หลาย type |
| wrap error | ห่อซองจดหมาย (%w) แกะดูไส้ได้ด้วย Is/As |
| context | รีโมทยกเลิกงาน |
| pipeline | สายพานโรงงาน (channel ต่อกัน) |
| fan-out/in | แตกหลายคนทำแล้วรวมผล |
| race | แย่งของกันจนพัง (ตรวจด้วย -race) |
| goroutine leak | จ้างแล้วลืมไล่กลับ ค้างตลอดไป |
| Green Tea GC | แม่บ้านเก็บขยะรุ่นใหม่ (1.26) ไวขึ้น |
| json/v2 | เครื่องแปลง JSON รุ่นใหม่ เร็วขึ้น (1.27) |
| go fix | ครูแก้โค้ดเก่าเป็นแบบใหม่ให้ |
เล่มกลาง ฉบับภาษาง่าย อัปเดตสำหรับ Go 1.27.1 ตุลาคม 2026 — ต่อจากเล่ม Beginner ถ้าอ่านแล้วอยากกลับไปทวน channel พื้นฐานเปิดเล่มก่อนตอนที่ 13 ได้เลย