1 · Go คืน error เป็นค่า ไม่ใช่ throw
ก่อนอื่น: ฟังก์ชัน Go คืนค่าได้มากกว่าหนึ่งตัว —
เขียน type ที่จะคืนไว้ในวงเล็บ คั่นด้วยจุลภาค เช่น (float64, error)
แปลว่า "คืนตัวเลขหนึ่งตัว และสถานะข้อผิดพลาดอีกหนึ่งตัว"
ตอนเรียกใช้ก็รับด้วยตัวแปรสองตัวคั่นจุลภาคเช่นกัน:
result, err := divide(10, 2) — ตัวซ้ายรับค่าแรก ตัวขวารับค่าที่สอง
เรียงตามลำดับใน signature เสมอ
signature = บรรทัดหัวของฟังก์ชัน ที่บอกว่ารับอะไรเข้าไปและคืนอะไรออกมา
(ในที่นี้คือ func divide(a, b float64) (float64, error) ·
พูดเคร่งครัดตามสเปกของ Go คำว่า signature หมายถึงเฉพาะส่วน
(a, b float64) (float64, error) ไม่นับคำว่า func และไม่นับชื่อฟังก์ชัน)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
// เรียกใช้ — สำนวนมาตรฐานที่จะเจอทุกวัน
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
บท 02 บอกว่าทุก type มี zero value — int ได้
0, string ได้ ""
nil คือ zero value ของ error
แปลว่า "ไม่มีอะไร / ว่างเปล่า"
ดังนั้น err != nil อ่านว่า "มี error อยู่จริงใช่ไหม"
ส่วน return a/b, nil อ่านว่า "คืนผลลัพธ์ และบอกว่าไม่มี
error" — ไม่ใช่การไม่คืนค่า แต่คืนค่าว่าง ๆ ที่แปลว่าสำเร็จ
กดถอนเงิน 5,000 บาท ตู้จะตอบกลับมา สองอย่างเสมอ:
· ซองเงิน — ของที่เราอยากได้
· สลิป — ใบที่บอกว่าเรียบร้อยไหม หรือติดตรงไหน
ถอนไม่สำเร็จ = ได้ซองเปล่า + สลิปที่เขียนว่า “ยอดเงินไม่พอ”
ตู้ไม่ได้ส่งเสียงเตือนทั้งธนาคาร — มันแค่ยื่นใบแจ้งกลับมาให้เราอ่านเอง
Go คิดแบบเดียวกันเป๊ะ ๆ: return a / b, nil คือ “เงิน + สลิปสะอาด” ·
return 0, errors.New(...) คือ “ซองเปล่า + สลิปที่มีข้อความ” ·
ส่วน if err != nil คือ การก้มอ่านสลิปก่อนเดินออกจากตู้
ที่ไม่เหมือนกัน: ตู้ ATM ยัดสลิปใส่มือเราไม่ว่าจะอยากได้หรือไม่
แต่ Go ไม่บังคับให้อ่าน — เขียน result, _ := divide(10, 0)
แล้วใช้ result ต่อ (เช่น fmt.Println(result)) ก็ยัง compile ผ่าน
ทั้งที่ทิ้งสลิปลงถังไปแล้ว
(ถ้าไม่ใช้ result ต่อเลย Go จะฟ้องว่า
declared and not used: result — นั่นเป็นคนละเรื่อง มันบ่นเรื่อง
ตัวแปรที่ประกาศแล้วไม่ได้ใช้ ไม่ใช่บ่นเรื่องไม่ได้เช็ค error)
นี่แหละคือเหตุผลที่ต้องท่องกฎข้างล่างนี้ให้ขึ้นใจ
errorเป็น return value ตัวสุดท้ายเสมอ- สำเร็จ = คืน
nilเป็น error - เช็ค
err != nilทันทีที่รับค่ามา — ไม่มีข้อยกเว้น
ถ้าฟังก์ชันไม่มีผลลัพธ์อื่นนอกจากสำเร็จ/ไม่สำเร็จ ก็คืนแค่ error ตัวเดียว
แล้วรวมกับ short statement ได้เลย
short statement = ท่าที่เขียนคำสั่งหนึ่งบรรทัดไว้ในหัว if เลย
แล้วคั่นด้วย ; ก่อนเงื่อนไข — เรียกฟังก์ชันกับเช็คผลจบในบรรทัดเดียว:
func saveUser(u User) error { ... }
if err := saveUser(user); err != nil {
fmt.Println("save failed:", err)
return
}
แบบนี้ err มีชีวิตแค่ในบล็อก if — ไม่ไปรกด้านนอก
และกันเผลอเอา err เก่ามาใช้ซ้ำ
บล็อก = ช่วงโค้ดที่อยู่ในปีกกา { } คู่หนึ่ง ·
ตัวแปรที่เกิดในบล็อกไหน จะตายพร้อมปีกกาปิดของบล็อกนั้น
· นอกจากปีกกาที่เห็นด้วยตา หัวของ if / for /
switch ที่มี short statement ก็นับเป็นบล็อกซ่อนอีกชั้นหนึ่งด้วย —
err ตัวข้างบนจึงเกิดในบล็อกซ่อนนั้น ทำให้ทั้งฝั่ง if และฝั่ง
else (ถ้ามี) มองเห็นมันได้ แต่โค้ดที่อยู่นอก if มองไม่เห็น
จำคำนี้ไว้ให้ดี เพราะหัวข้อ 3 ทั้งหัวข้อวนอยู่รอบเรื่องนี้
คำพวกนี้คือชื่อที่วงการใช้เรียกสิ่งที่เพิ่งเข้าใจไป — ใช้คำให้ตรง จะได้คำตอบที่ตรง ทั้งตอนถามรุ่นพี่และตอนถาม AI
- errors are values
- แนวคิด “ข้อผิดพลาดคือค่าธรรมดาที่ถูกคืนออกมา” = สลิปจากตู้ ATM · พูดว่า “Go จัดการ error แบบ return value ไม่ใช่ exception” คนฟังจะรู้ทันทีว่าเราเข้าใจโครงแล้ว · ค้นด้วยคำนี้เป๊ะ ๆ (ไม่ใช่ “errors as values”) จะเจอบทความต้นทางบน blog ของ Go
- exception / try-catch
- วิธีของภาษาอื่น (Java, Python, JS) ที่ “โยน” ข้อผิดพลาดขึ้นไปให้ใครข้างบนรับ · Go ไม่มีท่านี้ — พูดได้เลยว่า “Go ไม่มี exception”
- panic
- ตัวที่ใกล้ “โยน error” ที่สุดใน Go แต่สงวนไว้ตอนโปรแกรมไปต่อไม่ได้จริง ๆ · ถามว่า “เคสนี้ควร return error หรือควร panic”
- signature
- บรรทัดหัวของฟังก์ชัน — ส่วนที่บอกว่ารับอะไรและคืนอะไร
(พูดเคร่งครัดคือส่วน
(a, b float64) (float64, error)ไม่นับชื่อฟังก์ชัน — เวลาคุยเรื่อง function type หรือ interface จะยึดนิยามเคร่งครัดนี้) · ถามว่า “signature ของฟังก์ชันนี้เป็นยังไง” จะสื่อกว่า “ฟังก์ชันนี้ใช้ยังไง” - idiomatic Go
- “เขียนตามสำนวนที่ชาว Go เขาเขียนกัน” · ลองถาม AI ว่า “โค้ดนี้ idiomatic Go หรือยัง” จะได้คำแนะนำคนละแบบกับถามว่า “โค้ดนี้ผิดไหม”
2 · Named return & naked return
// ตั้งชื่อ return value ได้
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("cannot divide by zero")
return // ← naked return: คืน result, err ที่มีอยู่ตอนนั้น
}
result = a / b
return
}
divide(1, 0) คืนค่าอะไรออกมา?
0, "cannot divide by zero"
result ไม่เคยถูกเซ็ต จึงเป็น zero value = 0
— นี่คือจุดแข็งของ named return: ไม่ต้องเขียน return 0, err
ให้ครบทุกจุด
พอเรานั่งลง พนักงานฉีกใบสั่งอาหารมาวางบนโต๊ะก่อนที่เราจะสั่งอะไรเลย
ในใบมีสองช่องพิมพ์หัวไว้แล้ว: “รายการที่ได้” กับ “หมายเหตุ/ปัญหา”
ระหว่างนั้นเขาเดินมาเติมช่องไหนก็ได้ · พอถึงเวลาส่งเข้าครัว
เขาแค่หยิบใบนั้นยื่นออกไป ไม่ต้องคัดลอกใส่กระดาษใหม่
(result float64, err error) คือใบที่มีสองช่องนี้ ·
err = errors.New(...) คือการเดินมาเติมช่อง “ปัญหา” ·
ส่วน return เปล่า ๆ คือ “ยื่นใบนี้ออกไปตามที่กรอกไว้ ณ ตอนนั้น”
ไม่ต้องเขียน return 0, err ซ้ำทุกจุด
ที่ไม่เหมือนกัน: ช่องว่างบนกระดาษจริงคือ “ว่าง” ที่คนอ่านรู้ว่ายังไม่ได้กรอก
แต่ช่องที่ไม่ได้กรอกใน Go ไม่ได้ว่าง — มันมีค่าเริ่มต้นอยู่แล้ว
(float64 ได้ 0, error ได้ nil)
ปลายทางจึงอ่านได้ว่า “ราคา 0 บาท และไม่มีปัญหา” ทั้งที่ความจริงคือเราลืมกรอก
ถ้าฟังก์ชันยาว 40 บรรทัดแล้วเจอ return เปล่า ๆ
คนอ่านต้องเลื่อนขึ้นไปดู signature ว่าคืนอะไรบ้าง —
Effective Go บอกให้ใช้กับฟังก์ชันสั้นเท่านั้น
ฟังก์ชันยาวให้เขียนค่าออกมาให้ครบ
แต่ named return มีพลังพิเศษอีกอย่างที่คุ้มมาก —
defer เข้าไปแก้มันได้ (เรื่องนี้อยู่ใน
บทเรียน 08) ใช้ห่อ error ทุกจุด
return ในครั้งเดียว:
func loadConfig(path string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("loadConfig %s: %w", path, err)
}
}()
// ...มี return err อยู่ 5 จุด ทุกจุดถูกห่อให้อัตโนมัติ
}
// ได้: loadConfig app.yaml: file missing
- named return value
- “ใบที่มีช่องพิมพ์หัวไว้แล้ว” — ตั้งชื่อค่าที่จะคืนไว้ตั้งแต่ใน signature · เวลารีวิวโค้ดกันจะได้ยินว่า “ฟังก์ชันนี้ใช้ named return นะ ระวังตอนแก้”
- naked return
returnเปล่า ๆ ที่ไม่ระบุค่า = “ยื่นใบนั้นออกไปเลย” · ถ้ารุ่นพี่ทักว่า “อย่าใช้ naked return กับฟังก์ชันยาว” เขากำลังพูดถึงเรื่องนี้- zero value
- ค่าเริ่มต้นที่ Go ใส่ให้อัตโนมัติเมื่อยังไม่ได้กำหนดค่า
(
0,"",false,nil) · ถามว่า “zero value ของ type นี้คืออะไร” จะตรงกว่าถามว่า “มันเริ่มที่เท่าไหร่” - error wrapping (
%w) - ห่อ error เดิมไว้ข้างในพร้อมเติมบริบทว่าพังตอนไหน · ประโยคที่ใช้ได้จริง: “ผม wrap error ด้วย %w เพื่อให้ errors.Is เช็คต้นตอได้”
3 · 💀 กับดักใหญ่: := ไม่ได้แปลว่า "สร้างใหม่" เสมอไป
var sum = 0
if true {
sum := sum + 20
fmt.Println("ข้างใน =", sum)
}
fmt.Println("ข้างนอก =", sum)
พิมพ์อะไรออกมา?
ข้างใน = 20
ข้างนอก = 0 ← ไม่ใช่ 20!
sum := sum + 20 สร้างตัวแปร sum ตัวใหม่
ที่มีชีวิตแค่ใน block นั้น (ค่าเริ่มต้นคำนวณจาก sum ตัวนอก = 20)
พอออกจาก block ตัวใหม่ตาย sum ตัวนอกไม่เคยถูกแตะเลย
อาการนี้เรียกว่า variable shadowing — ตัวในบัง ตัวนอก
ที่บ้านมีกระดาษแปะตู้เย็นอยู่ใบหนึ่ง หัวกระดาษเขียนว่า “เงินกองกลาง” ตัวเลข 0 ใครใช้เงินก็เดินมาแก้เลขบนใบนี้ — ทั้งบ้านดูใบเดียวกัน
วันหนึ่งคุณเข้าไปในห้องนอน หยิบกระดาษใบใหม่มาเขียนหัวว่า “เงินกองกลาง” เหมือนกันเป๊ะ
แล้วจดเลข 20 ไว้ · ทำงานเสร็จ เดินออกจากห้อง กระดาษใบนั้นถูกทิ้งไปพร้อมกับที่คุณออกจากห้อง
กลับมาดูตู้เย็น — ยังเป็น 0 เหมือนเดิม เพราะไม่เคยมีใครไปแตะใบที่ตู้เย็นเลย
· sum := ... ในห้องอื่น (คนละบล็อก) =
หยิบกระดาษใบใหม่มาเขียนหัวชื่อเดิม
· sum = ... = เดินไปแก้เลขบนใบที่แปะตู้เย็น
ต่างกันแค่จุดเดียว (:) แต่คนละเรื่องกันเลย
อย่าเพิ่งเหมาว่า := แปลว่า “ใบใหม่” เสมอ — ถ้าชื่อซ้ำนั้น
อยู่ห้องเดียวกัน Go จะไม่หยิบใบใหม่ แต่แก้เลขบนใบเดิมให้
เงื่อนไขเต็ม ๆ อยู่ในกล่อง “ทำไมถึงหลบตาคุณได้” ข้างล่าง
ที่ไม่เหมือนกัน: ในชีวิตจริง ถ้ามีกระดาษสองใบ เราเดินไปหยิบใบไหนก็ได้
แต่ในโค้ด นับจากบรรทัดที่หยิบใบใหม่ขึ้นมาเป็นต้นไป
คุณเรียกถึงใบข้างนอกในห้องนั้นไม่ได้อีก —
ชื่อ sum ถูกใบในห้องยึดไปจนกว่าจะออกจากปีกกา
สังเกตคำว่า “นับจากบรรทัดนั้นเป็นต้นไป” ให้ดี — นี่คือเหตุผลที่
sum := sum + 20 ยังอ่านค่า 0 จากใบข้างนอกมาบวกได้
เพราะฝั่งขวาถูกคำนวณก่อนที่ใบใหม่จะเริ่มมีผล
รายละเอียดยิบ ๆ ระดับนี้แหละที่ทำให้บั๊กแบบนี้มองด้วยตาไม่ค่อยเห็น
แก้ง่ายมาก — เปลี่ยน := เป็น =
if true {
sum = sum + 20 // = ไม่ใช่ := → แก้ตัวนอกจริง
}
// ข้างนอก = 20 ✓
ทำไมถึงอันตราย: เวอร์ชัน err
อาการเดียวกันนี้เกิดกับ err ได้ และตรงนี้แหละที่ทำให้ production พัง
func process() error {
var err error
if needFile {
file, err := os.Open("demo.txt") // 💀 := สร้าง err ตัวใหม่!
if err != nil {
fmt.Println("Cannot open:", err)
}
_ = file
}
return err // ← คืน nil เสมอ! error ที่เกิดจริงหายไปแล้ว
}
ไล่ทีละสเต็ป:
- บรรทัดนั้นมี
fileซึ่งเป็นตัวแปรใหม่จริง ๆ จึงต้องใช้:= - Go มีกฎว่า
:=ใช้ได้ถ้ามีตัวแปรใหม่อย่างน้อย 1 ตัวทางซ้าย — ส่วนตัวที่ชื่อซ้ำ (err) จะถูก "ใช้ตัวเดิม" ให้ - แต่กฎข้อ 2 ใช้ได้เฉพาะตอนที่ตัวชื่อซ้ำอยู่ในบล็อกเดียวกัน —
ในโค้ดนี้
errตัวเดิมอยู่นอกปีกกาของifส่วนบรรทัดนี้อยู่ข้างใน ถือว่าคนละบล็อก - ผลคือ Go สร้าง
errตัวใหม่ขึ้นมาในบล็อกifซ้อนทับตัวนอก และตัวนอกยังเป็นnilอยู่จนจบฟังก์ชัน
แล้วทำไม compiler (ตัวแปลโค้ดของเราให้เป็นไฟล์ที่เครื่องรันได้ — คนที่คอยฟ้องว่าเราเขียนผิด)
ถึงเงียบ? เพราะ Go จะบ่นก็ต่อเมื่อประกาศตัวแปรแล้วไม่ได้ใช้
— ในที่นี้ err ตัวใหม่ถูกใช้ในบรรทัดถัดไปเรียบร้อย
ในสายตา compiler จึงไม่มีอะไรผิด
ทางแก้ 2 แบบ:
// 1) ประกาศตัวแปรใหม่แยกไว้ก่อน แล้วบรรทัดที่เรียกฟังก์ชันใช้ = ล้วน (ไม่มี :=)
var file *os.File // type ของ "ไฟล์ที่เปิดค้างไว้" — เครื่องหมาย * จะอธิบายตอนเรียน pointer
file, err = os.Open("demo.txt") // = ล้วน → err ตัวนอกถูกเซ็ตจริง
// 2) จัดการ error ให้จบในที่เดียวกับที่มันเกิด (แนะนำ — ตรงกับ guard clause)
file, err := os.Open("demo.txt")
if err != nil {
return err // ส่งออกไปเลย ไม่ต้องเก็บไว้ใช้ทีหลัง
}
ไม่ต้องพึ่งสายตาอย่างเดียว — เครื่องมือ
errcheck และ
shadow analyzer จับได้
analyzer = โปรแกรมที่อ่านโค้ดเราแทนสายตา แล้วฟ้องจุดที่น่าสงสัย
โดยไม่ต้องรันโปรแกรมจริง:
go vet -vettool=$(which shadow) ./...
หัวข้อนี้เป็นบั๊กที่ต้องขอความช่วยเหลือคนอื่นบ่อยที่สุด — ถ้าเรียกชื่ออาการถูก จะประหยัดเวลาไปได้เป็นชั่วโมง
- variable shadowing
- อาการ “ตัวในบังตัวนอก” = กระดาษใบใหม่ในห้องนอน · แทนที่จะบอกว่า “ค่ามันหายไปเฉย ๆ ไม่รู้ทำไม” ให้พูดว่า “ตรงนี้ err โดน shadow หรือเปล่า” — รุ่นพี่จะรู้ทันทีว่าต้องดูตรงไหน และ AI จะตอบตรงจุดขึ้นมาก
- scope
- “ขอบเขตที่ชื่อตัวแปรนั้นใช้ได้” = “ห้อง” ที่กระดาษใบนั้นแปะอยู่ ·
แต่ละ block (ปีกกา
{ }คู่หนึ่ง) สร้าง scope ใหม่ขึ้นมาหนึ่งชั้น · หัวของif/for/switchที่มี short statement ก็เป็น block อีกชั้นแม้ไม่มีปีกกาให้เห็น — เวลาไล่บั๊ก shadow อย่าลืมนับชั้นนี้ด้วย · คำถามมาตรฐานเวลาไล่บั๊กคือ “ตัวแปรตัวนี้อยู่ scope ไหน” - short variable declaration (
:=) - ชื่อทางการของ
:== “ประกาศตัวแปรใหม่พร้อมให้ค่า” · ต่างจาก assignment (=) ที่แปลว่า “เอาค่าไปใส่ตัวที่มีอยู่แล้ว” - redeclaration rule
- กฎที่ว่า
:=ใช้ซ้ำได้ถ้ามีตัวแปรใหม่อย่างน้อย 1 ตัวทางซ้าย และตัวที่ชื่อซ้ำอยู่บล็อกเดียวกัน · ถามได้เลยว่า “สองบรรทัดนี้อยู่ block เดียวกันไหม ถ้าคนละ block มันจะ redeclare ใช่หรือเปล่า” - static analysis
- การให้เครื่องมืออ่านโค้ดหาจุดผิดโดยไม่ต้องรันโปรแกรม ·
go vet,shadow,errcheckอยู่ในกลุ่มนี้หมด · พูดว่า “มี static analysis ตัวไหนจับเคสนี้ได้บ้าง”
4 · ทดสอบความจำ
1. ฟังก์ชัน Go ที่อาจล้มเหลว ควรออกแบบ signature ยังไง?
สำนวนมาตรฐานคือ (result T, err error) — panic
สงวนไว้สำหรับ "โปรแกรมพังจนไปต่อไม่ได้" เท่านั้น
(Don't Panic ใน Go Code Review Comments)
ส่วน bool ใช้กับ comma-ok ของ map/type assertion ซึ่งไม่ได้บอก "ทำไมถึงล้มเหลว"
2. var sum = 0 แล้วใน if เขียน sum := sum + 20 — ข้างนอกได้เท่าไหร่?
:= ใน block ใหม่ = สร้างตัวแปรใหม่ที่บังตัวนอก
ตัวนอกไม่เคยถูกแตะ ถ้าอยากแก้ตัวนอกต้องใช้ =
3. file, err := os.Open(f) ในที่ที่มี err อยู่แล้วข้างนอก — เกิดอะไรขึ้น?
ถ้าอยู่ คนละ scope จะสร้างใหม่ทั้งคู่ →
err ข้างนอกยังเป็น nil อยู่ ทำให้
return err ตอนท้ายคืน nil ทั้งที่เกิด error จริง
(ถ้าอยู่ scope เดียวกัน Go จะ reuse
err ตัวเดิมให้ ขอแค่มีตัวแปรใหม่อย่างน้อย 1 ตัวทางซ้าย —
สองกรณีนี้ต่างกันตรงนี้)
4. func f() (result int, err error) เขียน return เปล่า ๆ ได้อะไร?
naked return — คืนค่าปัจจุบันของ named return value
ถ้ายังไม่เคยเซ็ตก็ได้ zero value (0 และ nil)
5 · ลงมือทำ
ภารกิจ: calculatePrice (workshop จากสไลด์ Day 2)
เขียน func calculatePrice(price, discountPercent float64) (float64, error)
- ถ้า
price < 0→ คืน error"price cannot be negative" - ถ้า
discountPercent < 0หรือ> 100→ คืน error"discount must be between 0 and 100" - ถ้าถูกต้อง →
finalPrice = price − (price × discountPercent ÷ 100)
- เขียนด้วย guard clause จากบทที่แล้ว (ไม่มี
elseเลย) - ทดสอบ 3 เคส:
(1000, 10)→ 900.00 ·(-500, 10)→ error ·(1000, 120)→ error - ฝั่งผู้เรียก ต้องเช็ค
err != nilก่อนใช้ค่า และพิมพ์ด้วยfmt.Printf("Final price: %.2f\n", v) - เขียนใหม่อีกรอบด้วย named return + naked return เทียบดูว่าชอบแบบไหน
- ท้าทาย 1 (ลอกแบบก่อน เข้าใจทีหลัง): ทำให้ error ทุกจุดขึ้นต้นด้วย
"calculatePrice: "โดยไม่ต้องไปแก้ทีละจุด return — เปลี่ยน signature เป็น named return แล้ววางก้อนนี้ไว้บรรทัดแรกของฟังก์ชัน:
รู้ได้ยังไงว่าถูก: เรียกด้วยfunc calculatePrice(price, discountPercent float64) (final float64, err error) { defer func() { if err != nil { err = fmt.Errorf("calculatePrice: %w", err) } }() // ...guard clause เดิมทั้งหมด }(-500, 10)แล้วพิมพ์ error ออกมา ต้องได้calculatePrice: price cannot be negative(มีคำนำหน้าแล้ว) ส่วนเคส(1000, 10)ต้องยังได้ 900.00 เหมือนเดิม - ท้าทาย 2 (สร้างบั๊กเอง): จงใจเขียนโค้ดที่มี shadowing ของ
errให้ฟังก์ชันคืนnilทั้งที่ควรเป็น error — แล้วรันgo vet ./...ดูว่ามันจับได้ไหม
สปอยล์:go vetเปล่า ๆ จับไม่ได้ — ผมรันทดสอบแล้ว เงียบสนิท ต้องลง shadow analyzer แยกต่างหาก บทเรียนของข้อนี้คือ เครื่องมือ default ไม่ได้กันให้ทุกอย่าง สายตาคุณยังต้องทำงานgo install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest go vet -vettool=$(which shadow) ./...
ต่อไปเวลาเห็น := ข้างใน if/for ที่มี
err อยู่ทางซ้าย คุณจะหยุดคิดหนึ่งวินาทีว่า
"นี่กำลังสร้างตัวใหม่หรือใช้ตัวเดิม" — วินาทีนั้นแหละที่กันบั๊กระดับ production
อ่านต่อจากต้นทาง
แหล่งหลักที่แนะนำ:
Error handling and Go —
บทความทางการที่อธิบายว่าทำไม Go ถึงเลือกคืน error เป็นค่า
ต่อด้วย Working with Errors in Go 1.13
ถ้าอยากเข้าใจ %w, errors.Is, errors.As
💬 ติดตรงไหนถามได้เลย — โดยเฉพาะข้อ 3 ของ quiz
(:= scope เดียวกัน vs คนละ scope) เป็นจุดที่ละเอียดมาก
ถ้ายังไม่ชัดถามมาได้ เดี๋ยวทำตัวอย่างเทียบให้ดูทีละบรรทัด
อ้างอิง
- Error handling and Go — ทำไมถึงคืน error เป็นค่า
- Working with Errors in Go 1.13 —
%w,errors.Is,errors.As - Go spec — Short variable declarations — กฎ "redeclaration" ที่ทำให้
:=reuse ตัวแปรได้เฉพาะใน scope เดียวกัน - Effective Go — Named result parameters
- Go Code Review Comments — Don't Panic