ข้าม​ไป​ยัง​เนื้อหา

slice map string และ rune

สาม​บท​แรก​ทำให้ orderctl อ่า​นอ​อเด​อร์​จาก JSON แล้ว​บอก​ยอด​รวม​ได้ · บท​นี้​เริ่ม​ทำ​ใบ​สรุป แยก​ตาม​หมวด และ​นั่น​คือ​จุด​ที่ collection ของ Go เริ่ม​กัด

กับ​ดัก​ของ​บท​นี้​ไม่ใช่ syntax ที่​แปลก · มัน​คือ​บรรทัด​ที่​หน้าตา​เหมือน​ภาษา​ที่​คุณ​เขียน​อยู่ แต่​ทำ​คนละ​อย่าง

📦 บท​นี้​เพิ่ม​อะไร​ให้ orderctl

internal/pricing — TotalsByCategory รวม​ยอด​ต่อ​หมวด · SortLines เรียง​บรรทัด​ออ​เด​อร์ · Render ผลิต​ใบ​สรุป​เป็น​ข้อความ

ข้อ​กำหนด​ข้อ​เดียว​ที่​บังคับ​ทั้ง package คือ ใบ​สรุป​ต้อง​เหมือน​เดิม​ทุก​ครั้ง​ที่​รัน และ​ข้อ​นั้น​เอง​ที่​บังคับ​ให้​เรา​จัดการ​กับ​ลำดับ​ของ map

array คัด​ลอก​ทั้ง​ก้อน slice คัด​ลอก​แค่​สาม​ช่อง​ที่​ชี้​ไป​ที่​ก้อน​เดิม บรรทัด​มอบ​หมาย​ค่า​จึง​ให้​ผล​ตรง​ข้าม​กัน

slice headerslice headerค่า​สาม​ช่อง (pointer, ความ​ยาว, ความ​จุ) ที่​แทน slice — การ​คัด​ลอก slice คือ​การ​คัด​ลอก​สาม​ช่อง​นี้ ไม่ใช่​คัด​ลอก​ข้อมูล สอง slice จึง​แชร์​ที่​เก็บ​เดียวกัน​ได้ คือ​ค่า​สาม​ช่อง — pointer ไป​ยัง​ที่​เก็บ, ความ​ยาว และ​ความ​จุ · ตัว slice ไม่​ได้​ถือ​ข้อมูล​เอง มัน​ถือ​ที่​อยู่​ของ​ข้อมูล

[3]int กับ []int ต่าง​กัน​แค่​ตัวเลข​ใน​วงเล็บ แต่​พฤติกรรม​ตอน​มอบ​หมาย​ค่า​ตรง​ข้าม​กัน​สนิท

package main
import "fmt"
func main() {
arr := [3]int{1, 2, 3}
brr := arr // array เป็น value คัดลอกทั้งก้อน
brr[0] = 99
fmt.Println(arr, brr) // [1 2 3] [99 2 3] ← ต่างกัน
s := []int{1, 2, 3}
t := s // slice คัดลอกแค่สามช่องของ header
t[0] = 99
fmt.Println(s, t) // [99 2 3] [99 2 3] ← ตัวเดียวกัน
}
ถ้า​คุณ​มา​จาก C# Java หรือ Python

คุณ​คุ้น​กับ​กฎ​ที่​ว่า collection เป็น reference เสมอ · Go ไม่มี​กฎ​นั้น

[3]int เป็น value เต็ม​ตัว การ​ส่ง​เข้า​ฟังก์ชัน​คือ​การ​คัด​ลอก​ทั้ง​ก้อน · []int เป็น header ที่​ชี้​ออก​ไป การ​ส่ง​เข้า​ฟังก์ชัน​คัด​ลอก​แค่​สาม​ช่อง

ผล​คือ​ฟังก์ชัน​ที่​รับ []int แก้​ข้อมูล​ของ​ผู้​เรียก​ได้ แต่​ฟังก์ชัน​ที่​รับ [3]int แก้​ไม่​ได้​เลย

append เขียน​ทับที่​ว่าง​ใน​ความ​จุ​เดิม​ได้ ซึ่ง​เป็น​ที่​ที่ slice ตัว​อื่น​ยัง​ใช้​อยู่ และ full slice expression คือ​ตัว​ปิด​ช่อง​นั้น

base[:2] ไม่​คัด​ลอก​อะไร​เลย · มัน​สร้าง header ใหม่​ที่​ชี้​ไป​ที่ array เดิม ความ​ยาว​เป็น 2 แต่ ความ​จุ​ยัง​เป็น 5

append ตัดสิน​ใจ​จาก​ความ​จุ · ถ้า​ยัง​เหลือ​ที่ มัน​เขียน​ลง​ที่​เดิม ไม่​จอง​ที่​ใหม่

package main
import (
"fmt"
"slices"
)
func main() {
base := []int{1, 2, 3, 4, 5}
head := base[:2]
fmt.Println(len(head), cap(head)) // 2 5 ← ความจุยาวกว่าความยาว
head = append(head, 99)
fmt.Println(base) // [1 2 99 4 5] ← base โดนเขียนทับ
safe := []int{1, 2, 3, 4, 5}
tail := safe[:2:2] // full slice expression ตรึงความจุไว้ที่ 2
tail = append(tail, 99)
fmt.Println(safe, tail) // [1 2 3 4 5] [1 2 99]
clipped := slices.Clip(base[:2]) // เขียนแบบอ่านง่ายกว่า ได้ผลเดียวกัน
clipped = append(clipped, 7)
fmt.Println(base, clipped) // [1 2 99 4 5] [1 2 7]
}

บรรทัด base[:2:2] มี​ตัวเลข​สาม​ตัว — เริ่ม, จบ, และ​เพดาน​ความ​จุ · เมื่อ​ความ​จุ​เท่ากับ​ความ​ยาว append ไม่มี​ที่​ว่าง​ให้​เขียน มัน​จึง​ต้อง​จอง​ที่​ใหม่​เสมอ

flowchart LR
  S1["slice head<br/>pointer · len 2 · cap 5"] --> ARR
  S2["slice base<br/>pointer · len 5 · cap 5"] --> ARR
  ARR["backing array ก้อนเดียว<br/>[1 2 3 4 5]<br/>ไม่มีการคัดลอกตอนตัด"]
  ARR --> W["append ผ่าน head<br/>ยังเหลือความจุ จึงเขียนช่องที่สาม<br/>ซึ่ง base ยังใช้อยู่"]

คำ​บรรยาย​ภาพ: การ​ตัด slice สร้าง header ใหม่​ที่​ชี้​ไป​ที่ backing array ก้อน​เดิม · append ที่​ยัง​มี​ความ​จุ​เหลือ​จะ​เขียน​ลง​ก้อน​นั้น จึง​กระทบ slice ตัว​อื่น​ที่​ชี้​อยู่​ที่​เดียวกัน

กฎ​ที่​กัน​เรื่อง​นี้​ได้​จริง​มี​ข้อ​เดียว

append คืน​ค่า​เสมอ และ​คุณ​ต้อง​รับ​ค่า​นั้น​กลับ​ไป​ใส่​ตัวแปร​เดิม

append(s, x) ที่​ไม่​รับ​ค่า​คืน คือ​บรรทัด​ที่ compiler ปฏิเสธ เพราะ​ผลลัพธ์​ถูก​ทิ้ง

ส่วน​บรรทัด​ที่​อันตราย​กว่า​คือ t := append(s, x) แล้ว​ใช้​ทั้ง s และ t ต่อ — มัน​คอม​ไพล์​ผ่าน และ​ให้​ผล​ถูก​บ้าง​ผิด​บ้าง​ตาม​ความ​จุ​ที่​เหลือ ณ ตอน​นั้น

การ​อ่าน​ไม่​เคย​ล้ม ต้อง​ใช้​รูป​สอง​ค่า​เพื่อ​แยก “ไม่มี” ออก​จาก “มี​แต่​เป็น​ศูนย์” และ map ที่​ยัง​ไม่​ถูก make เขียน​ไม่​ได้

Go ไม่​โยน​อะไร​ออก​มา​เมื่อ​คุณ​อ่าน​คีย์​ที่​ไม่มี · มัน​คืน zero value ของ​ชนิด​ค่า · รูป​สอง​ค่า v, ok := m[k] คือ​ทาง​เดียว​ที่​แยก​สอง​กรณี​นี้​ออก​จาก​กัน

nil mapnil mapmap ที่​ยัง​ไม่​ถูก `make` อ่าน​ได้ วน​ได้ `len` ได้ แต่​การ​เขียน​ทำให้ panic — ต่าง​จาก nil slice ที่ `append` ได้​ตาม​ปกติ คือ map ที่​ประกาศ​แล้ว​แต่​ยัง​ไม่ make · อ่าน​ได้ วน​ได้ len ได้ แต่​การ​เขียน​ทำให้ panic

package main
import "fmt"
func main() {
qty := map[string]int{"phad-thai": 2}
n, ok := qty["tom-yum"]
fmt.Println(n, ok) // 0 false ← ไม่มีคีย์ ก็ยังไม่ล้ม
qty["tom-yum"] += 3 // อ่าน zero value แล้วบวก สำนวนคิดยอดของ Go
delete(qty, "phad-thai")
fmt.Println(qty, len(qty)) // map[tom-yum:3] 1
var empty map[string]int
fmt.Println(empty["x"], len(empty)) // 0 0 ← อ่าน nil map ได้ตามปกติ
empty["x"] = 1 // panic: assignment to entry in nil map
}

บรรทัด m[k] += v ทำงาน​ได้​เพราะ​การ​อ่าน​คีย์​ที่​ยัง​ไม่มี​ให้​ศูนย์ · คุณ​จึง​ไม่​ต้อง​เช็​กก่อ​นบ​วก และ​นั่น​คือ​ทั้งหมด​ของ TotalsByCategory

สิ่ง​ที่​คุณ​คาด​จาก​ภาษา​เดิมGo ทำ​จริง
KeyNotFoundException หรือ KeyError ตอน​อ่าน​คีย์​ที่​ไม่มีคืน zero value เงียบๆ ต้อง​ใช้ v, ok := m[k] เพื่อ​รู้
null แยก​จาก 0 ได้zero value ของ int คือ 0 แยก​ไม่​ออก​ถ้า​ไม่​ใช้​รูป​สอง​ค่า
ประกาศ​แล้ว​ใช้ได้​เลยvar m map[string]int เขียน​แล้ว panic ต้อง make หรือ​ใช้ literal ก่อน
ลบ​คีย์​ที่​ไม่มี​แล้ว​ล้มdelete กับ​คีย์​ที่​ไม่มี​คือ no-op ไม่​ล้ม
nil slice กับ nil map ไม่​เหมือน​กัน

var s []int ที่​เป็น nil รับ append ได้​ทันที เพราะ append จอง​ที่​ใหม่​ให้​เอง

var m map[string]int ที่​เป็น nil เขียน​ไม่​ได้ เพราะ​ไม่มี​ฟังก์ชัน​ไหน​ที่​จอง​ที่​ให้​แทนคุณ

ผล​ใน​ทาง​ปฏิบัติ​คือ struct ที่​มี field เป็น slice ใช้ zero value ได้​เลย แต่ struct ที่​มี field เป็น map ต้อง make ก่อน​เขียน​เสมอ

Go สุ่ม​ลำดับ​โดย​ตั้งใจ กฎ​ที่​ต้อง​จำ​มี​ข้อ​เดียว​คือ​เรียง​คีย์​ก่อน​ทุก​ครั้ง​ที่​ผล​ไหล​ออก​ไป​เป็น output

สเปก​ของ​ภาษา​เขียน​ไว้​ตรงๆ ว่า​ลำดับ​ของ​การ​วน map ไม่​ถูก​กำหนด และ​ไม่​รับ​ประกัน​ว่า​จะ​เหมือน​เดิม ระหว่าง​การ​วน​สอง​ครั้ง

นี่​ไม่ใช่​ผล​ข้าง​เคียง​ของ​การ​จัด​เก็บ · runtime สุ่ม​จุด​เริ่ม​ต้น​ให้​ตั้งใจ เพื่อ​ไม่​ให้​ใคร​เผลอ​พึ่ง​ลำดับ

package main
import (
"fmt"
"maps"
"slices"
)
func main() {
byCat := map[string]int{"food": 3, "drink": 2, "dessert": 1}
for cat, n := range byCat {
fmt.Println("ไม่รับประกันลำดับ:", cat, n)
}
for _, cat := range slices.Sorted(maps.Keys(byCat)) {
fmt.Println("คงที่ทุกครั้ง:", cat, byCat[cat])
}
}

ลูป​แรก​ให้​ลำดับ​ที่​คุณ​เดา​ไม่​ได้ · ลูป​ที่​สอง​ให้ dessert drink food ทุก​ครั้ง​บน​ทุก​เครื่อง

อย่า​อธิบาย​เรื่อง​นี้​ด้วย​โมเดล 'มี​ไม่​กี่​ลำดับ'

คน​ที่​รัน​ลูป​แรก​สอง​สาม​รอบ​แล้ว​เห็น​ผล​ซ้ำ มัก​สรุป​ว่า map เล็ก​มี​ลำดับ​จำกัด​ไม่​กี่​แบบ แล้ว​เขียน test ที่​ยอมรับ​ลำดับ​เหล่า​นั้น

โมเดล​นั้น​พัง​ทันที​ที่ map โต​ขึ้น และ​มัน​พัง​ใน​รูป​ของ test ที่​แดง​เดือน​หน้า​โดย​ไม่มี​ใคร​แก้​อะไร

กฎ​ที่​ใช้ได้​จริง​มี​ข้อ​เดียว — ผล​ที่​ไหล​ออก​ไป​เป็น output ต้อง​ผ่าน​การ​เรียง​คีย์​ก่อน​เสมอ ไม่​ว่า map จะ​เล็ก​แค่​ไหน

สาม​ชุด​นี้​มา​กับ Go 1.21 · slices.SortStableFunc คือ​ตัว​ที่มา​แทน sort.Slice และ comparator ของ​มัน​คืน int ไม่ใช่ bool

Go 1.21 เพิ่ม slices maps และ cmp เข้า standard library · Go 1.22 เพิ่ม cmp.Or ที่​คืน​ค่า​แรก​ที่​ไม่ใช่ zero value

cmp.Or ทำให้​การ​เรียง​ด้วย​กุญแจ​สอง​ชั้น​เขียน​ได้​โดย​ไม่​ต้อง​มี if ซ้อน

package main
import (
"cmp"
"fmt"
"slices"
)
type line struct {
SKU string
Category string
}
func main() {
lines := []line{
{"tom-yum", "food"},
{"cola", "drink"},
{"phad-thai", "food"},
}
slices.SortStableFunc(lines, func(a, b line) int {
return cmp.Or( // ชั้นแรกเท่ากันเมื่อไร ค่อยดูชั้นที่สอง
cmp.Compare(a.Category, b.Category),
cmp.Compare(a.SKU, b.SKU),
)
})
fmt.Println(lines) // [{cola drink} {phad-thai food} {tom-yum food}]
i := slices.IndexFunc(lines, func(l line) bool { return l.SKU == "tom-yum" })
fmt.Println(i, slices.Contains([]string{"food", "drink"}, "dessert")) // 2 false
}

slices.SortStableFunc แก้ slice ใน​ที่ และ​ไม่​คืน​ค่า · มัน​ไม่ใช่ chain แบบ LINQ หรือ Stream ที่​คืน​ของ​ใหม่​ให้​ต่อ​ท้าย

comparator ของ​มัน​คืน int ไม่ใช่ bool — ติดลบ​เมื่อ a มา​ก่อน, ศูนย์​เมื่อ​เท่า​กัน, บวก​เมื่อ a มา​ทีหลัง

sort.Slice ยัง​ใช้ได้​และ​ยัง​ไม่​ถูก​ประกาศ​เลิก​ใช้ แต่​เอกสาร​ของ​มัน​แนะนำ​เอง​ว่า​ให้​ไป​ใช้ slices.SortFunc แทน เพราะ​เขียน​ง่าย​กว่า​และ​เร็ว​กว่า​ใน​หลาย​กรณี

`maps.Keys` ไม่​ได้​คืน slice

ตั้งแต่ Go 1.23 maps.Keys คืน iter.Seq[K] ซึ่ง​เป็น​ฟังก์ชัน ไม่ใช่ slice

คุณ​จึง​ส่ง​มัน​เข้า slices.Sorted ได้​ตรงๆ แต่​จะ len() มัน​ไม่​ได้ และ index มัน​ไม่​ได้

ตัวอย่าง​เก่า​จาก golang.org/x/exp/maps คืน slice จริง · ถ้า​คุณ​ลอก​มา​แล้ว​เจอ error เรื่อง​ชนิด นั่น​คือ​สาเหตุ

ของ​ที่​มี​ใน standard library แล้ว อย่า​เขียน​เอง — slices.Index, slices.Contains และ maps.Clone ครอบคลุม​งาน​ที่​คน​เขียน​ลูป​เอง​บ่อย​ที่สุด

ทำไม​บท​นี้​ไม่​สอน `slices.BinarySearchFunc`

BinarySearchFunc ใช้ได้​เฉพาะ​เมื่อ comparator ที่​ใช้​ค้น เป็น​ตัว​เดียว​กับ​ที่​ใช้​เรียง

ใน​บท​นี้​เรา​เรียง​ด้วย​กุญแจ​สอง​ชั้น (Category แล้ว SKU) แต่​การ​ค้น​จริง​มัก​ค้น​ด้วย SKU อย่าง​เดียว · การ​ผสม​สอง​แบบ​นี้​ให้​คำ​ตอบ​ผิด​โดย​ไม่มี error ทั้ง​ที่​รายการ​นั้น​มี​อยู่​จริง

ใช้ slices.IndexFunc แทน · มัน​ไม่​ต้องการ slice ที่​เรียง​แล้ว จึง​ไม่มี​เงื่อนไข​ซ่อน​ให้​ผิด

package pricing รวม​ยอด​ต่อ​หมวด เรียง​คีย์​ก่อน​พิมพ์ และ​มี test ที่​เรียก Render ซ้ำ​เพื่อ​บังคับ​ว่า​ผล​ต้อง​เหมือน​เดิม

นี่​คือ internal/pricing/report.go ทั้ง​ชิ้น · มัน​ใช้​ทุก​อย่าง​ใน​บท​นี้​พร้อม​กัน

// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ชนิด order.Order มาจาก package ของบทที่ 2
package pricing
import (
"cmp"
"fmt"
"maps"
"slices"
"strings"
"example.com/orderctl/internal/order"
)
func TotalsByCategory(o order.Order) map[string]order.Baht {
out := make(map[string]order.Baht, len(o.Lines))
for _, l := range o.Lines {
out[l.Category] += l.Unit * order.Baht(l.Qty) // ใช้ได้เพราะค่าเป็นตัวเลข
}
return out
}
func SortLines(lines []order.Line) {
slices.SortStableFunc(lines, func(a, b order.Line) int {
return cmp.Or(
cmp.Compare(a.Category, b.Category),
cmp.Compare(a.SKU, b.SKU),
)
})
}
func Render(o order.Order) string {
var b strings.Builder // zero value ใช้ได้ทันที ห้ามคัดลอกหลังเขียนแล้ว
fmt.Fprintf(&b, "order %s (โต๊ะ %d)\n", cmp.Or(o.ID, "-"), o.Table)
byCat := TotalsByCategory(o)
for _, cat := range slices.Sorted(maps.Keys(byCat)) { // เรียงคีย์ก่อนเสมอ
fmt.Fprintf(&b, " %-10s %10s\n", cat, byCat[cat])
}
fmt.Fprintf(&b, " %-10s %10s\n", "TOTAL", o.Subtotal())
return b.String()
}

strings.Builder เป็น zero value ที่​ใช้ได้​ทันที ไม่​ต้อง make · มัน​จอง​ที่​เพิ่ม​เป็น​ช่วงๆ แทน​การ​จอง​ใหม่​ทุก​ครั้ง​ที่​ต่อ​ข้อความ​ด้วย +

ถ้า​คุณ​ต่อ slice ของ​ข้อความ​ด้วย​ตัว​คั่น​ตัว​เดียว strings.Join ตรง​กว่า เพราะ​มัน​รู้​ความ​ยาว​รวม ก่อน​เริ่ม จึง​จอง​ที่​ครั้ง​เดียว

test ที่​บังคับ​ข้อ​กำหนด​ของ package นี้ ไม่​ได้​เทียบ​กับ​ข้อความ​ที่​เขียน​ตายตัว · มัน​เรียก​ซ้ำ​แล้ว​เทียบ​กับ​ผล​ของ​ตัว​เอง

// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · demo() สร้างออเดอร์ตัวอย่างที่มีสามหมวด
func TestRenderIsStable(t *testing.T) {
first := Render(demo())
for range 50 {
if got := Render(demo()); got != first {
t.Fatalf("Render() ไม่คงที่:\n%s\nเทียบกับ\n%s", got, first)
}
}
}

ถอด​บรรทัด slices.Sorted ออก​จาก Render แล้ว test นี้​จะ​แดง · นั่น​คือ​หลัก​ฐาน​ว่า​มัน​คุม​ของ​จริง ไม่ใช่​ผ่าน​เพราะ​ไม่​ได้​ตรวจ​อะไร

len ให้​จำนวน​ไบต์ การ​วน range ให้​ที​ละ rune และ​ข้อความ​ไทย​แสดง​ให้​เห็น​ว่า​สอง​เลข​นี้​ไม่​เท่า​กัน

string ใน Go คือ​ลำดับ​ไบต์​ที่​แก้​ไม่​ได้ · มัน​ไม่​ได้​บังคับ​ว่า​ต้อง​เป็น UTF-8 แต่​ทุก​อย่าง ใน​ภาษา​และ standard library ทำงาน​บน​สมมติฐาน​นั้น

runeruneชื่อ​สั้น​ของ Unicode code point เป็น alias ของ `int32` การ​วน `range` บน string เดิน​ที​ละ rune ไม่ใช่​ที​ละ​ไบต์ คือ​ชื่อ​ของ Unicode code point · บล็อก​ทางการ​ของ Go เขียน​ว่า “The Go language defines the word rune as an alias for the type int32”

package main
import (
"fmt"
"strconv"
"unicode/utf8"
)
func main() {
s := "ผัดไทย"
fmt.Println(len(s)) // 18 ← จำนวนไบต์
fmt.Println(utf8.RuneCountInString(s)) // 6 ← จำนวน rune
fmt.Println(s[0]) // 224 เป็น uint8 ไม่ใช่ ผ
fmt.Println(string([]rune(s)[0])) // ผ
for i, r := range s {
fmt.Printf("%d U+%04X ", i, r)
}
fmt.Println() // 0 U+0E1C 3 U+0E31 6 U+0E14 9 U+0E44 12 U+0E17 15 U+0E22
for i, r := range "a\xffb" {
fmt.Printf("%d U+%04X ", i, r)
}
fmt.Println() // 0 U+0061 1 U+FFFD 2 U+0062 ← ไบต์เสียได้ตัวแทน ไม่ panic
fmt.Println(string(rune(65)), strconv.Itoa(65)) // A 65
}

สังเกต​ว่า index ของ​ลูป range กระโดด​ที​ละ 3 ไม่ใช่​ที​ละ 1 · มัน​คือ​ตำแหน่ง​ไบต์ ไม่ใช่​ลำดับ​ที่​ของ​ตัว​อักษร

แม้แต่ rune ก็​ยัง​ไม่ใช่ 'ตัว​อักษร'

"ผัดไทย" มี 18 ไบต์ และ 6 rune · แต่​ตา​คน​ไทย​นับ​ช่อง​ได้ 5 ช่อง

เพราะ ั ที่ U+0E31 เป็น combining mark มัน​เกาะ​อยู่​บน​พยัญชนะ​ตัว​หน้า ไม่​กิน​ที่​ของ​ตัว​เอง

ผล​คือ “จำนวน​ตัว​อักษร​ที่​ผู้​ใช้​เห็น” ไม่มี​อยู่​ใน​ภาษา​เลย · ถ้า​คุณ​ต้อง​ตัด​ข้อความ​ให้​พอดี​ช่อง คุณ​ต้องหา library ที่​รู้จัก grapheme cluster ไม่ใช่​นับ rune

`string(65)` ไม่​ได้​แปล​ว่า 65 ที่​เป็น​ข้อความ

ใน Java, C# หรือ Python การ​แปลง​ตัวเลข​เป็น​ข้อความ​ให้ "65" · Go ให้ "A" เพราะ​มัน​ตีความ​ตัวเลข​เป็น code point

go vet จับ​รูป​นี้​ให้​ทั้ง​เมื่อ​เป็น​ตัวแปร​และ​เมื่อ​เป็น​ค่า​คงที่ แต่​ข้อความ​เรียก​ชื่อ​ชนิด​ต้นทาง​ตาม​จริง จึง​ไม่ใช่​ข้อความ​เดียวกัน

string(n) ที่ n เป็น int ได้ conversion from int to string · string(65) ที่​เป็น ค่า​คงที่​ยัง​ไม่มี​ชนิด ได้ conversion from untyped int to string · ท่อน​ท้าย​เหมือน​กัน​ทั้ง​คู่​คือ yields a string of one rune, not a string of digits

และ​เพราะ​ตัว​ตรวจ​นี้​อยู่​ใน​ชุด​ปริยาย​ของ go test การ​เขียน​แบบ​นี้​ทำให้ go test ล้ม​ด้วย build failed ไม่ใช่​แค่​เตือน · ใช้ strconv.Itoa เมื่อ​ต้องการ​ตัวเลข​เป็น​ข้อความ

บท​นี้​จบ​ด้วย package pricing ที่​ให้​ผล​คงที่ และ​กฎ​สาม​ข้อ​ที่​ใช้ได้​กับ​ทุก​งาน​ที่​มี collection

สาม​ข้อ​ที่​ควร​ติดตัว​ไป​บท​หน้า:

  • slice คือ header สาม​ช่อง การ​ตัด​ไม่​คัด​ลอก · ใช้ s[:n:n] หรือ slices.Clip เมื่อ​ไม่​อยาก​ให้​ผู้รับ​เขียน​ทับ​ของ​เดิม
  • map ไม่มี​ลำดับ เรียง​คีย์​ก่อน​ทุก​ครั้ง​ที่​ผล​ไหล​ออก​ไป​เป็น output และ make ก่อน​เขียน​เสมอ
  • string เป็น​ไบต์ ใช้ range หรือ []rune เมื่อ​ต้องการ code point และ​อย่า​เชื่อ​ว่า len บอก​จำนวน​ตัว​อักษร

บท​หน้า​เป็น interface · Render ตอน​นี้​เรียก o.Subtotal() ตรงๆ บท​ที่ 5 จะ​เปลี่ยน บรรทัด​นั้น​เป็น Total(o, rules...) หนึ่ง​ครั้ง

หลัง​การ​เปลี่ยน​ครั้ง​เดียว​นั้น กฎ​ส่วนลด​กฎ​ใหม่​จะ​เพิ่ม​ได้​โดย​ไม่​ต้อง​แตะ Render อีก​เลย · นั่น​คือ​สิ่ง​ที่ interface ซื้อ​ให้


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

  • The Go Programming Language Specification (2026-05-26) — หัวข้อ Slice types, Slice expressions (รวม Full slice expressions ที่​นิยาม a[low:high:max]), Appending and copying slices และ For statements ที่​ระบุ​ว่า​ลำดับ​การ​วน map ไม่​ถูก​กำหนด
  • Go 1.21 Release Notes (2023-08-08) — slices maps cmp เข้า standard library พร้อม builtin min max clear
  • Go 1.22 Release Notes (2024-02-06) — cmp.Or และ​การ​เคลียร์​หาง​ของ slices.Delete กับ slices.Compact
  • pkg.go.dev — maps (go1.23.0, 2024-08-13) — maps.Keys คืน iter.Seq[K] ไม่ใช่ slice
  • Strings, bytes, runes and characters in Go (2013-10-23) — “The Go language defines the word rune as an alias for the type int32”
  • pkg.go.dev — sort (เข้า​ถึง 2026-08-27) — เอกสาร​ของ sort.Slice แนะนำ​เอง​ให้​ไป​ใช้ slices.SortFunc
  • stringintconv analyzer (เข้า​ถึง 2026-08-27) — ตัว​ตรวจ​ที่​จับ string(int) และ​อยู่​ใน​ชุด​ปริยาย​ของ go test

เช็กความเข้าใจ — บทที่ 4

ข้อ 1 / 3

`base := []int{1,2,3,4,5}` แล้ว `head := base[:2]` ตามด้วย `head = append(head, 99)` เกิดอะไรขึ้นกับ base