เรียน Go ภาษาง่าย 📘

🗂️

เจาะลึก Slices + Maps + Struct ฉบับภาษาง่าย

append, clone, map + lock, embedding

9 บท
14 ข้อสอบ
1. Array vs Slice
go
var a [3]int // array: ตู้ปูน 3 ช่อง เปลี่ยนจำนวนไม่ได้ [3]int กับ [5]int คนละชนิด
a[0] = 10

s := []int{1, 2, 3} // slice: กระเป๋าผ้ายืดได้ (ใช้ตัวนี้ 95%)

กฎ: เขียนโค้ดทั่วไปใช้ slice อย่าใช้ array ยกเว้นอยากได้ก้อนนิ่งๆ เช่น [32]byte เก็บ hash

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


2. Slice คือกระดาษโน้ต 3 ค่า

นึกว่า slice = โน้ตแปะว่า "ของอยู่บ้านไหน ยาวเท่าไหร่ จุได้เท่าไหร่" (ptr/len/cap):

go
s := make([]int, 2, 5) // len=2 cap=5
fmt.Println(len(s), cap(s)) // 2 5

กับดัก: ก็อปโน้ต = ชี้บ้านเดิม!

go
a := []int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a) // [99 2 3] โดนด้วย!

ส่งเข้า func ก็เหมือนกัน แก้ข้างใน caller เห็นด้วย (เพราะชี้บ้านเดียวกัน) แต่ถ้า append จนย้ายบ้าน caller จะไม่เห็นถ้าไม่ return กลับ

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


3. append + copy + หั่น (slicing)
go
s := []int{1, 2, 3}
s = append(s, 4, 5) // ต้องมี s = ข้างหน้า! เพราะอาจย้ายบ้าน
y := []int{6, 7}
s = append(s, y...) // ... = แกะกล่องโปรยเข้าไป

// อยากได้บ้านใหม่จริงๆ
d := make([]int, len(s))
copy(d, s)
// หรือสั้นๆ
c := append([]int{}, s...)

// หั่น
buf := []int{1, 2, 3, 4, 5}
fmt.Println(buf[0:2])  // [1 2]
fmt.Println(buf[2:])   // [3 4 5]
fmt.Println(buf[:3])   // [1 2 3]

ระวังหั่นแล้วยังชี้บ้านเดิม แก้ฝั่งนึงอีกฝั่งโดนด้วย ถ้าจะให้ขาดให้ copy หรือใช้ slices.Clone (Go 1.21+):

go
import "slices"
c2 := slices.Clone(s)
slices.Sort(c2)
slices.Compact(c2) // ตัดตัวซ้ำติดกัน

go fix ตัวใหม่ slicesbackward จะช่วยแก้ loop วนกลับหลังให้ใช้ slices.Backward ที่ถูกและเร็วขึ้น (Go 1.26+)

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


4. Slice 2 มิติ + ของใหม่ compiler 1.26

Go มีแค่ 1 มิติ ต้องซ้อน:

go
// วิธี 1: แยกบ้านทีละแถว (แถวโตได้อิสระ)
pic := make([][]uint8, 3)
for i := range pic { pic[i] = make([]uint8, 4) }

// วิธี 2: บ้านเดียวแล้วหั่น (ประหยัด ถ้าขนาดคงที่)
pic2 := make([][]uint8, 3)
pix := make([]uint8, 3*4)
for i := range pic2 { pic2[i], pix = pix[:4], pix[4:] }

ข่าวดี 1.26: compiler เอาที่เก็บ slice ไปไว้บน stack ได้บ่อยขึ้น = เร็วขึ้น กินแรมน้อยลง โดยไม่ต้องแก้โค้ด ดูได้ด้วย go build -gcflags='-m'

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


5. Map สมุดโทรศัพท์ + comma-ok
go
m := map[string]int{"UTC": 0, "EST": -18000}
m["CST"] = -21600
delete(m, "EST") // ลบตัวไม่มีก็ไม่พัง

v := m["xxx"] // ไม่มีได้ 0 (zero value) ต้องแยกด้วย comma-ok
v, ok := m["xxx"]
if !ok { fmt.Println("ไม่มี key นี้") }

key ได้ทุก type ที่ == ได้ (string int struct) แต่ slice ใช้เป็น key ไม่ได้ เพราะเทียบไม่ได้

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


6. Map + Concurrency ต้องล็อก

สมุดเล่มเดียว 2 คนเขียนพร้อมกันขาด (race):

go
var mu sync.Mutex
mu.Lock()
m[k] = v
mu.Unlock()

// อ่านเยอะเขียนน้อยใช้ RWMutex
mu.RLock()
v := m[k]
mu.RUnlock()

// หรือใช้ sync.Map
var sm sync.Map
sm.Store(k, v)
v, ok := sm.Load(k)

ตรวจเสมอ go test -race ./...

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


7. Struct + Embedding + ของใหม่ 1.27

struct = แฟ้มรวมกล่อง:

go
type Person struct {
    Name string
    Age  int
}
p := Person{Name: "Ann", Age: 20}

Embedding = ยัดกล่องในกล่อง เรียกของข้างในได้เลย:

go
type Logger struct{}
func (Logger) Log(s string) { fmt.Println(s) }
type Job struct {
    Command string
    Logger
}
j := Job{Command: "run"}
j.Log("start")

ของใหม่ 1.27: สร้างแบบฝังยัดไส้ชั้นในได้เลย:

go
type Habitat struct{ Burrow string }
type Gopher struct {
    Name string
    Habitat
}
g := Gopher{Name: "Gopher", Burrow: "Burrow #42"} // ไม่ต้องห่อ Habitat{...} แล้ว

go fix embedlit ช่วยเปลี่ยนแบบเก่าเป็นแบบใหม่ให้ + new(T) แบบใหม่ 1.26 ใส่ค่าได้เลย p := new(int64(300))

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ภาคผนวก: ตัวอย่างเพิ่ม + Best Practices
ตัวอย่างเพิ่ม 1: กับดัก append แชร์ array
go
a := []int{1, 2, 3}
b := append(a[:2], 9) // b แชร์ array เดียวกับ a!
fmt.Println(a)        // [1 2 9] <- a โดนเปลี่ยนด้วย ไม่รู้ตัว
c := append([]int(nil), a[:2]...) // แบบนี้ปลอดภัย (clone ก่อน)
_ = c
ตัวอย่างเพิ่ม 2: หั่นแบบล็อก cap (ไม่กระทบต้นฉบับ)
go
buf := make([]byte, 0, 1024)
chunk := buf[:0:0] // cap=0: append ทีหลังไม่กระทบ buf แน่นอน
_ = chunk
ตัวอย่างเพิ่ม 3: map กันพังด้วย RWMutex
go
type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func (s *SafeMap) Get(k string) (int, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    v, ok := s.m[k]
    return v, ok
}
Best practices เช็กลิสต์
  • append อาจเปลี่ยน array ต้นฉบับ ถ้าจะเก็บแยกให้ clone ก่อน
  • ส่ง slice ใหญ่ๆ ระวังแชร์ทั้งก้อนโดยไม่ตั้งใจ (pass by header)
  • map อ่านพร้อมกันได้ เขียนพร้อมกันต้องล็อก/ใช้ channel
  • เช็ก key ด้วย comma-ok v, ok := m[k] อย่าเดาจาก zero value
  • key ต้อง comparable อย่าใช้ slice เป็น key
  • preallocate ด้วย make(..., 0, n) เมื่อรู้ขนาด ลด alloc
  • struct ใหญ่ๆ เก็บเป็น pointer ใน slice/map ประหยัด copy
แบบฝึกหัด
  • เขียน CloneInts + เทสต์ว่าแก้ตัว clone แล้วต้นฉบับไม่เปลี่ยน
  • ทำ SafeMap แบบมี Set/Delete/Keys ครบ + เทสต์ -race

อ้างอิง: Effective Go #slices #maps #embedding, Go 1.26 (stack alloc, new(expr)), Go 1.27 (embedlit)