1 คะแนน โดย GN⁺ 2025-06-04 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • แม้การเขียน if err!= nil ซ้ำ ๆ ใน Go จะเป็นความไม่พอใจใหญ่ในแบบสำรวจผู้ใช้มาหลายปี แต่ทีม Go ตัดสินใจว่าจะยังไม่ผลักดัน การเปลี่ยนแปลงไวยากรณ์การจัดการข้อผิดพลาด ในตอนนี้
  • ข้อเสนอ check/handle ในปี 2018, try ในปี 2019 และ ? ในปี 2024 ต่างก็ไม่ได้รับฉันทามติเพียงพอ โดยเฉพาะ try ที่ถูกคัดค้านอย่างหนักเพราะมี control flow ที่ซ่อนอยู่
  • ตามกระบวนการข้อเสนอของ Go หากไม่มีฉันทามติทั่วไป ข้อเสนอมักจะถูกปฏิเสธ และแม้แต่ในหมู่สมาชิกอาวุโสของทีม Go ที่ Google เองก็ยังไม่มีความเห็นเป็นเอกฉันท์เกี่ยวกับ ทิศทางที่ดีที่สุด ในปัจจุบัน
  • ฝ่ายที่ต้องการคงสภาพเดิมเห็นว่า Go มีวิธีจัดการข้อผิดพลาดที่ใช้งานได้อยู่แล้ว และไวยากรณ์ใหม่จะสร้างต้นทุนจำนวนมากต่อสไตล์โค้ด การดีบัก เอกสาร เครื่องมือ และโค้ดเดิม
  • ทีม Go จะปิด ข้อเสนอที่เปิดอยู่และข้อเสนอใหม่ ที่มีเป้าหมายหลักเป็นไวยากรณ์การจัดการข้อผิดพลาดโดยไม่ตรวจสอบเพิ่มเติม และจะมุ่งไปที่โอกาสปรับปรุงด้านอื่นจนกว่าจะเกิดความเข้าใจปัญหาที่ชัดเจนขึ้น

ความไม่พอใจเรื้อรังที่เกิดจาก if err != nil

  • หนึ่งในความไม่พอใจที่ยืดเยื้อที่สุดใน Go คือความ ยืดยาว ของโค้ดจัดการข้อผิดพลาด
  • รูปแบบที่พบได้ทั่วไปเป็นดังนี้
x, err := call()
if err != nil {
    // handle err
}
  • ในโปรแกรมที่มีการเรียก API จำนวนมากและเพียงแค่คืนค่าข้อผิดพลาดต่อไป if err != nil อาจกินพื้นที่เด่นกว่าโค้ดส่วนที่เหลือ
  • ในฟังก์ชันตัวอย่าง printSum จากเนื้อหาฟังก์ชัน 10 บรรทัด มีเพียง 4 บรรทัดที่ดูเหมือนเป็นงานจริง รวมถึงการเรียกฟังก์ชัน การพิมพ์ผล และการคืนค่า ส่วนอีก 6 บรรทัดดูเหมือน noise
  • ในแบบสำรวจผู้ใช้ประจำปีของ Go การจัดการข้อผิดพลาดเป็นความไม่พอใจอันดับต้น ๆ มาหลายปี ช่วงหนึ่งการไม่มี generics เคยแซงขึ้นไป แต่หลังจาก Go รองรับ generics แล้ว การจัดการข้อผิดพลาดก็กลับมาเป็นความไม่พอใจอันดับต้น ๆ อีกครั้ง

ข้อเสนอไวยากรณ์หลักสามครั้ง

  • ความพยายามอย่างชัดเจนครั้งแรกของทีม Go เริ่มขึ้นในปี 2018 ในฐานะส่วนหนึ่งของความพยายาม Go 2 เมื่อ Russ Cox สรุปปัญหาอย่างเป็นทางการ
  • ร่างการออกแบบ ของ Marcel van Lohuizen อิงกับกลไก check และ handle และยังรวมการวิเคราะห์แนวทางของภาษาอื่นและทางเลือกต่าง ๆ ด้วย
func printSum(a, b string) error {
    handle err { return err }
    x := check strconv.Atoi(a)
    y := check strconv.Atoi(b)
    fmt.Println("result:", x + y)
    return nil
}
  • แนวทาง check/handle ถูกมองว่าซับซ้อนเกินไป และในปี 2019 ก็มี try proposal ที่เรียบง่ายกว่าออกมา
    • คีย์เวิร์ดที่คล้าย check กลายเป็น built-in function ชื่อ try
    • ส่วน handle ถูกตัดออก
    • มีการสร้างเครื่องมือ tryhard เพื่อแปลงโค้ดจัดการข้อผิดพลาดเดิมให้เป็นแบบ try
    • GitHub issue ที่เกี่ยวข้องถูกถกเถียงอย่างดุเดือดจนมีคอมเมนต์เกือบ 900 รายการ
func printSum(a, b string) error {
    // use a defer statement to augment errors before returning
    x := try(strconv.Atoi(a))
    y := try(strconv.Atoi(b))
    fmt.Println("result:", x + y)
    return nil
}
  • try ส่งผลต่อ control flow ด้วยการคืนค่าจากฟังก์ชันที่ครอบอยู่เมื่อเกิดข้อผิดพลาด และการคืนค่าสามารถเกิดขึ้นได้แม้ในนิพจน์ที่ซ้อนลึก ทำให้ผู้ใช้จำนวนมากยอมรับได้ยาก
  • ในเวลานั้น การเพิ่มคีย์เวิร์ดใหม่อาจเป็นทางเลือกที่ดีกว่า และปัจจุบันสามารถควบคุมเวอร์ชันภาษาได้ละเอียดขึ้นผ่านไฟล์ go.mod และ directive รายไฟล์
  • ข้อเสนอล่าสุด ของ Jimmy Frasche ย้อนกลับไปยังการออกแบบ check/handle ดั้งเดิม พร้อมจัดการข้อเสียบางส่วน

การทบทวนกระบวนการหลัง try และข้อเสนอ ?

  • หลังข้อเสนอ try Russ Cox ได้ทบทวนกระบวนการข้อเสนอผ่านชุดบทความ “Thinking about the Go Proposal Process”
  • “Go Proposal Process: Large Changes” ประเมินว่า try ควรเป็นร่างการออกแบบฉบับที่สอง ไม่ใช่ข้อเสนอที่มีตารางการ implement ติดมาด้วย
  • หลายปีต่อมา ทีม Go ไม่ได้ผลักดันการเปลี่ยนแปลงไวยากรณ์การจัดการข้อผิดพลาด และในชุมชนก็ยังมีข้อเสนอที่คล้ายกัน น่าสนใจ เข้าใจยาก หรือเป็นไปไม่ได้ส่งเข้ามาเรื่อย ๆ
  • Ian Lance Taylor สร้าง umbrella issue เพื่อสรุปสถานะของข้อเสนอปรับปรุงการจัดการข้อผิดพลาด และยังมี Go Wiki สำหรับรวบรวม feedback และการถกเถียงที่เกี่ยวข้อง
  • “go error handling proposals” ของ Sean K. H. Liao ติดตามข้อเสนอเกี่ยวกับการจัดการข้อผิดพลาดจำนวนมากตลอดหลายปี
  • เมื่อความไม่พอใจยังคงอยู่ Ian Lance Taylor จึงเผยแพร่ ข้อเสนอในปี 2024 ที่ใช้ ? เพื่อลด boilerplate การจัดการข้อผิดพลาด
    • เป็นสัญลักษณ์ที่ยืมมาจาก ? operator ของ Rust
    • ในการศึกษาผู้ใช้แบบไม่เป็นทางการขนาดเล็ก ผู้เข้าร่วมส่วนใหญ่เดาความหมายของโค้ด Go ที่ใช้ ? ได้ถูกต้อง
    • ยังมีการสร้างเครื่องมือสำหรับแปลงโค้ด Go ปกติเป็นไวยากรณ์ใหม่ และ prototype ของ compiler ด้วย
func printSum(a, b string) error {
    x := strconv.Atoi(a) ?
    y := strconv.Atoi(b) ?
    fmt.Println("result:", x + y)
    return nil
}
  • ข้อเสนอนี้ก็เต็มไปด้วยคอมเมนต์จำนวนมากและข้อเสนอปรับรายละเอียดตามความชอบส่วนตัวอย่างรวดเร็ว Ian จึงปิดข้อเสนอแล้วบรรจุเนื้อหาไปไว้ใน discussion
  • เวอร์ชันที่ปรับเล็กน้อยได้รับ เสียงตอบรับเชิงบวกมากขึ้นเล็กน้อย แต่ก็ยังไม่ได้รับการสนับสนุนอย่างกว้างขวาง

ทำไมตอนนี้จึงต้องการหยุด

  • ทีม Go เห็นว่าควรหยุดความพยายามแก้ ปัญหาเชิงไวยากรณ์ ของการจัดการข้อผิดพลาดในอนาคตที่คาดการณ์ได้
  • กระบวนการข้อเสนอ สนับสนุนการตัดสินใจนี้
    • เป้าหมายของกระบวนการข้อเสนอคือการบรรลุฉันทามติทั่วไปต่อผลลัพธ์อย่างทันท่วงที
    • หากในการถกเถียงใน issue tracker ไม่พบฉันทามติทั่วไป ข้อเสนอมักจะถูกปฏิเสธ
    • หากไม่พบทั้งฉันทามติและขั้นตอนถัดไป Go architects จะตรวจสอบการถกเถียงและพยายามหาฉันทามติภายใน
  • ไม่มีข้อเสนอด้านการจัดการข้อผิดพลาดใดได้รับการสนับสนุนที่ใกล้เคียงฉันทามติ และทั้งหมดถูกปฏิเสธ
  • แม้แต่สมาชิกอาวุโสของทีม Go ที่ Google ก็ยังไม่มีความเห็นเป็นเอกฉันท์เกี่ยวกับ ทิศทางการเดินหน้าที่ดีที่สุด ในปัจจุบัน และหากไม่มีฉันทามติที่แข็งแรงก็ไม่สามารถเดินหน้าได้อย่างสมเหตุสมผล

เหตุผลของการคงสภาพเดิมและการเปลี่ยนแปลง

  • ฝั่งคงสภาพเดิมมีเหตุผลเชิงปฏิบัติเรื่องความเติบโตเต็มที่ของ Go และต้นทุนต่อ ecosystem
    • หาก Go เพิ่ม syntactic sugar เฉพาะสำหรับการจัดการข้อผิดพลาดตั้งแต่ช่วงแรก ตอนนี้คงมีข้อถกเถียงน้อยกว่า แต่ Go มีอายุ 15 ปีแล้วและมีวิธีจัดการข้อผิดพลาดที่ใช้งานได้อยู่แล้ว
    • แม้ตอนนี้จะพบทางออกที่สมบูรณ์แบบ สถานการณ์ก็อาจเปลี่ยนเป็นฝ่ายที่ชอบคงสภาพเดิมเป็นผู้ไม่พอใจแทนฝ่ายที่สนับสนุนการเปลี่ยนแปลง
    • Generics เป็นสิ่งที่ผู้ใช้ไม่จำเป็นต้องเขียนเองโดยตรงเสมอไป แต่ไวยากรณ์ใหม่สำหรับการจัดการข้อผิดพลาด หากไม่ใช้ โค้ดอาจดูไม่เป็น idiomatic ทำให้ในทางปฏิบัติคนส่วนใหญ่ต้องใช้
    • การเพิ่มไวยากรณ์ใหม่อาจขัดกับกฎการออกแบบของ Go ที่ไม่เสนอหลายวิธีในการทำสิ่งเดียวกัน
  • ความสามารถ redeclare ของ short variable declaration := ถูกนำเข้ามาเพื่อแก้ปัญหาที่เกิดจากการจัดการข้อผิดพลาด
    • หากไม่มี redeclaration การตรวจข้อผิดพลาดต่อเนื่องแต่ละครั้งต้องใช้ชื่อ err ที่ต่างกันหรือประกาศตัวแปรแยกต่างหาก
    • หากตอนนั้นมีการรองรับไวยากรณ์สำหรับการจัดการข้อผิดพลาดที่ดีกว่า กฎ redeclaration และความซับซ้อนที่เกี่ยวข้องก็อาจไม่จำเป็น
  • หากเสริมข้อมูลให้ข้อผิดพลาดอย่างเหมาะสม สัดส่วนของการเขียนซ้ำแบบง่าย ๆ จะลดลง
    • ในแบบสำรวจผู้ใช้ มีความเห็นซ้ำ ๆ ว่าข้อผิดพลาดไม่มี stack trace
    • สามารถใช้ helper function สร้างและคืนค่าข้อผิดพลาดที่เสริมข้อมูลแล้วได้
    • หากแนบข้อมูล input เช่น fmt.Errorf("invalid integer: %q", a) สัดส่วนสัมพัทธ์ของ boilerplate ก็จะเล็กลง
  • ฟีเจอร์ใน standard library ก็ช่วยลด boilerplate ในการจัดการข้อผิดพลาดได้
    • เป็นแนวทางเดียวกับ “Errors are values” ของ Rob Pike
    • ในบางกรณีสามารถใช้ cmp.Or เพื่อจัดการหลายข้อผิดพลาดพร้อมกันได้
  • การเขียน การอ่าน และการดีบักเป็นกิจกรรมที่แตกต่างกัน
    • การเขียนการตรวจข้อผิดพลาดซ้ำ ๆ เป็นเรื่องน่าเบื่อ แต่ IDE และการเติมโค้ดด้วยความช่วยเหลือจาก LLM สามารถสร้างการตรวจข้อผิดพลาดพื้นฐานได้ง่าย
    • ตอนอ่าน ความยืดยาวจะเด่นชัดกว่า และ IDE อาจมี toggle สำหรับซ่อนโค้ดจัดการข้อผิดพลาด
    • ตอนดีบัก หากมี statement if แยกอยู่แล้ว การเพิ่ม println หรือการตั้ง breakpoint ก็ทำได้ง่าย
    • หากการจัดการข้อผิดพลาดถูกซ่อนไว้หลัง check, try, ? ก็มักอาจต้องแปลงกลับเป็น statement if และขั้นตอนนี้อาจทำให้การดีบักซับซ้อนขึ้นหรือก่อบั๊กละเอียดอ่อนได้
  • การเปลี่ยนแปลงภาษาไม่ได้มีต้นทุนแค่การออกแบบและ implement แต่ยังรวมถึงการเปลี่ยนโค้ดเดิม การอัปเดตเอกสาร และการปรับเครื่องมือด้วย
    • ทีม Go มีขนาดค่อนข้างเล็ก และยังมีลำดับความสำคัญอื่นอีกมากที่ต้องจัดการ
    • ลำดับความสำคัญและขนาดทีมอาจเปลี่ยนแปลงได้
  • ผู้ใช้ Go บางส่วนที่ทีม Go พบในงาน Google Cloud Next 2025 กล่าวอย่างหนักแน่นว่าไม่ควรเปลี่ยนภาษาเพื่อให้การจัดการข้อผิดพลาดดีขึ้น
    • พวกเขาบอกว่าเมื่อเพิ่งย้ายมาจากภาษาอื่น การไม่มีไวยากรณ์เฉพาะสำหรับการจัดการข้อผิดพลาดใน Go จะสะดุดตาที่สุด แต่เมื่อเริ่มเขียนโค้ด Go ที่ idiomatic มากขึ้น เรื่องนี้จะสำคัญน้อยลง
    • กลุ่มตัวอย่างนี้ไม่ได้ใหญ่พอจะมีความเป็นตัวแทน แต่ก็อาจเป็นกลุ่มคนละแบบกับที่เห็นบน GitHub
  • เหตุผลของฝ่ายสนับสนุนการเปลี่ยนแปลงก็ยังคงมีน้ำหนัก
    • การขาดการรองรับการจัดการข้อผิดพลาดที่ดีกว่ายังคงเป็นความไม่พอใจอันดับต้น ๆ ในแบบสำรวจผู้ใช้
    • แนวทางที่มุ่งลดจำนวนอักขระเพียงอย่างเดียวอาจเป็นทิศทางที่ผิด
    • หากทำให้การจัดการข้อผิดพลาดพื้นฐานมองเห็นได้ชัดด้วยคีย์เวิร์ด พร้อมกำจัด boilerplate err != nil ก็อาจช่วยให้ตรวจสอบใน code review ได้ง่ายขึ้นว่ามีการจัดการข้อผิดพลาดหรือไม่
    • ยังไม่ทราบเพียงพอว่าแก่นของปัญหาคือความยืดยาวของไวยากรณ์ง่าย ๆ หรือความยืดยาวของการจัดการข้อผิดพลาดที่ดี ซึ่งต้องประกอบข้อผิดพลาดให้มีความหมายต่อ API นักพัฒนา และผู้ใช้ปลายทาง

การตัดสินใจของทีม Go

  • จนถึงตอนนี้ ไม่มีความพยายามใดในการจัดการข้อผิดพลาดที่ได้รับแรงส่งเพียงพอ
  • ทีม Go เห็นว่ายังขาดความเข้าใจปัญหาร่วมกัน และทุกคนก็ไม่ได้เห็นตรงกันตั้งแต่แรกด้วยซ้ำว่ามีปัญหาอยู่หรือไม่
  • ในอนาคตที่คาดการณ์ได้ จะไม่ผลักดัน การเปลี่ยนแปลงภาษาเชิงไวยากรณ์ สำหรับการจัดการข้อผิดพลาด
  • ข้อเสนอที่เปิดอยู่และข้อเสนอที่จะเข้ามาในอนาคต ซึ่งมีเป้าหมายหลักเป็นไวยากรณ์การจัดการข้อผิดพลาด จะถูกปิดโดยไม่มีการตรวจสอบเพิ่มเติม
  • การสำรวจและถกเถียงของชุมชนแม้ไม่ได้นำไปสู่การเปลี่ยนแปลงไวยากรณ์การจัดการข้อผิดพลาด แต่ก็นำไปสู่การปรับปรุงหลายด้านของภาษา Go และกระบวนการของ Go

1 ความคิดเห็น

 
GN⁺ 2025-06-04
ความคิดเห็นใน Hacker News
  • ถ้าอยากโยนข้อเสนอแบบเบา ๆ ว่าทีม Go “น่าจะทำแบบนี้ก็จบแล้ว” ก็อยากให้ดูก่อนที่หน้า Wiki ที่ลิงก์ไว้ในบทความ https://go.dev/wiki/Go2ErrorHandlingFeedback และการค้นหา issue บน GitHub https://github.com/golang/go/issues?q=+is%3Aissue+label%3Aerror-handling
    สิ่งที่กำลังจะเสนอแทบจะแน่นอนว่าไม่ใช่ของใหม่ และหลายอย่างน่าจะถูกพิจารณาอย่างลึกซึ้งไปแล้ว
    ผมมองแนวทางที่ตรงไปตรงมาของทีม Go นี้ในแง่ดี และยังใช้ Go ทำงานทุกวันอย่างมีความสุข

    • ในร่างการออกแบบที่เป็นฐานของฟีดแบ็กมีการพูดถึง C++, Rust, Swift แต่ในเอกสารฟีดแบ็กขนาดมหึมาที่ลิงก์ไว้ ผมหาแนวทางอย่าง do notation, for-comprehension, monadic-let ที่ใช้ใน Haskell/Scala/OCaml ไม่เจอ
      ใน GitHub issue ที่มีคอมเมนต์มากที่สุดหลายหน้าก็ไม่เห็นอะไรคล้ายกัน และคงฝืนเกินไปที่จะคิดว่าทีม Go เป็นพ่อมดด้านการออกแบบภาษา จึงต้องเคยพิจารณาคำตอบที่คนแถวนี้โยนกันแบบเบา ๆ ไปแล้วแน่นอน
      ทีม Go เคยทำผิดแบบเดียวกับ Java คือเป็น static type แต่ไม่มี parametric polymorphism และรากของปัญหาการจัดการ error นี้ก็อยู่ตรงนั้น แต่ดูเหมือนพวกเขายอมแพ้และไม่แก้
    • น่าแปลกที่ทั้ง ๆ ที่คนฉลาดและมีประสบการณ์สูงมากเขียนหน้านั้นและถกกันมาหลายปี กลับไม่เห็นคำตอบแบบ Haskell อย่าง Maybe/Either monad และ do notation ที่ใช้ bind operator อยู่ที่ไหนเลย
      มันอาจฟังดูใหญ่โตและน่ากลัว แต่เป็นวิธีที่สง่างามและบริสุทธิ์ในเชิงฟังก์ชัน สำหรับส่งต่อ error ไปจนถึงจุดที่จัดการได้ โดยไม่ปล่อยให้ถูกลืม
      สำหรับคนที่เขียนโค้ด Haskell วิธีนี้ฝังลึกมาก จึงเข้าใจยากว่าในชุมชน Go จะไม่มีใครรู้จักและชอบมันเลย
      ขอบคุณสำหรับตัวหน้าเว็บและลิงก์ แต่ก็น่าสับสนที่คนซึ่งใส่ใจภาษาของตัวเองขนาดนั้นกลับข้ามคำตอบที่ตั้งหลักได้ดีแบบนี้ไป
    • คงมีคำตอบอยู่ที่ไหนสักแห่งแล้ว แต่สงสัยว่าทำไมมันถึงเป็น ปัญหาที่ยากเป็นพิเศษเฉพาะใน Go
      แทบทุกภาษามีวิธีที่ดีกว่าของตัวเอง เลยอยากรู้ว่าเป็นแค่เพราะตัดสินใจไม่ได้หรือทำให้ทุกคนพอใจไม่ได้ หรือมีเหตุผลเฉพาะของตัวภาษา Go เองที่ทำให้คำตอบของภาษาอื่นไม่เหมาะ
    • รูปแบบที่เห็นบ่อยในการวิจารณ์ Go คือคนที่ค่อนข้างเป็นมือสมัครเล่นสันนิษฐานว่าคนที่สร้าง Go รู้เรื่องภาษาโปรแกรมมิ่งน้อยกว่าตัวเอง
      ในความเป็นจริง แทบทุกกรณีพวกเขารู้มากกว่ามาก
      มือสมัครเล่นมักคิดอย่างใสซื่อว่าภาษาที่อัดฟีเจอร์เข้าไปมากที่สุดคือภาษาที่ดีที่สุด โดยเฉพาะถ้ามีฟีเจอร์ที่ตัวเองชอบอยู่ด้วย
      คล้ายกับคนที่เพิ่งเริ่มหัดทำมีด เห็นมีดเชฟญี่ปุ่นแล้วรู้สึกว่ามันยังขาดอะไรไป จากนั้นคิดว่าถ้าใส่ร่องนิ้ว ช่องลับ ไฟแช็ก และด้ามพิมพ์ 3D ที่มีลำโพง Bluetooth เข้าไปน่าจะดีกว่า
    • แปลกที่ยังเรียกว่า Wiki ทั้งที่ตอนนี้ถ้าจะแก้ไขต้องได้รับอนุมัติแล้ว
  • ทำรายการเช็กบ็อกซ์ขึ้นมา ถกแต่ละข้อแล้วเติมให้ครบ จากนั้นอย่าเอาออกอีก เว้นแต่จะพบข้อผิดพลาดเชิง semantics ร้ายแรงหรือช่องโหว่ด้าน soundness
    พอเติมครบก็ implement ไป แล้วคนที่เคยเถียงกันว่าจะใช้เป็น .await หรือ /await หรือ .await!() ก็จะหายกลับไปเอง
    Rust ทำงานแบบนี้ บาง issue อาจล่าช้าเกิน 10 ปี แต่สุดท้ายรายการก็ถูกเติมครบและถูก stabilize จาก nightly ล่าสุด
    ถ้า Go แก้ปัญหาเดี่ยวที่ทุกคนชนทันทีไม่ได้ ทั้งที่มีข้อเสนอที่สมบูรณ์สูงอยู่หลายแบบ แต่เลือกไม่ถูกสักอันและรอให้การ bikeshedding หยุด ขั้นตอนนั้นก็เป็นเรื่องตลก

    • ไม่มีสิ่งที่เรียกว่า “มีข้อเสนอที่สมบูรณ์แบบเสร็จแล้วหลายแบบ” หรอก
    • นั่นแหละคือวิธีที่ Rust ได้ชื่อว่าเป็นภาษาที่อ่านแล้วไม่สวยและไวยากรณ์ไม่สอดคล้องกัน เพราะ การออกแบบแบบคณะกรรมการ
    • ภาษาโปรแกรมมิ่งเป็นระบบที่ถูกออกแบบมา จึงต้องสมเหตุสมผลโดยรวม
      ไม่ใช่ชุดฟีเจอร์ที่พอผ่านข้อกำหนดแบบเช็กบ็อกซ์แล้วก็แค่เพิ่มเข้าไปได้
    • อยากตั้งเตือนอีก 25 ปีข้างหน้า
      Rust จะกลายเป็นความยุ่งเหยิงเหมือน C++ ไหม? Go จะยังเป็น ภาษาที่ไม่ล้าสมัยตามกาลเวลา เหมือนตอนเปิดตัวไหม?
    • คำว่า “Go แก้ปัญหาเดี่ยวที่ทุกคนเจอทันทีไม่ได้” ฟังแปลก
      ในแบบสำรวจ มีสัดส่วนที่พูดถึง การจัดการ error อยู่ 13% และก็มีคนที่ชอบวิธีปัจจุบันแบบเดิมอยู่ด้วย
      https://go.dev/blog/survey2024-h1-results
  • ก่อนหน้านี้เคยเขียนฟังก์ชัน Go แปลก ๆ ที่คาดหวังให้ฟังก์ชันภายในคืนข้อผิดพลาด
    ดังนั้นถ้าฟังก์ชันภายในไม่คืนข้อผิดพลาด ฟังก์ชันภายนอกก็ต้องคืนข้อผิดพลาดและจัดการอย่างอื่น และถ้าฟังก์ชันภายในคืนข้อผิดพลาด ก็ต้องคืน nil
    สรุปคือไม่ใช่ if err != nil { ... } แต่ต้องเขียน if err == nil { // return an error } ทว่าเผลอเขียนแบบแรกตามความเคยชิน และใช้เวลาดีบักอยู่นานพอสมควร
    เพราะชินชากับ if err != nil มากเกินไป จนสมองไม่คิดถึงความเป็นไปได้เลยว่าประโยคนั้นไม่ควรอยู่ตรงนั้น
    ดังนั้นจึงคิดว่าสำนวนที่พบบ่อยควรมี syntactic sugar หากความแตกต่างระหว่าง if err != nil ที่พบบ่อยมาก กับ if err == nil ที่พบไม่บ่อย เด่นชัดกว่านี้ ก็คงช่วยได้จริง

    • ทุกครั้งที่เขียน if err == nil จะใส่คอมเมนต์ // inverted ไว้ให้สะดุดตา
      ถ้าจัดการได้ในระดับภาษาก็คงดี แต่ขอแชร์ไว้เป็นอย่างน้อยว่าเป็นวิธีทำให้มองเห็นได้ชัดขึ้น
    • แน่นอนว่า if fruit != "Apple" { ... } ก็สร้างสถานการณ์แบบเดียวกันได้
      เลยสงสัยว่ามีวิธีแก้ทั่วไปเพื่อปรับปรุงเรื่องนี้ไหม และการมองว่านี่เป็นปัญหาเฉพาะของการจัดการข้อผิดพลาดก็ดูจะคลาดเคลื่อนไปหน่อย
      ข้อผิดพลาดไม่ได้มีอะไรพิเศษหรือเฉพาะตัว เป็นเพียงสถานะหนึ่งเหมือนอย่างอื่นเท่านั้น
    • เรื่องนี้กลับเป็นเหตุผลคัดค้านการเปลี่ยนไวยากรณ์เสียมากกว่า
      เพราะถ้าเกิดแพตเทิร์น if err == nil { return ... } ที่ใช้กันบ่อยขึ้น คราวนี้มันก็จะกระจายอยู่เต็มโค้ดแทน
      วิธีแก้ปัจจุบันก็ใช้ได้ดี และดูเหมือนว่าคนที่ไม่ชอบมักเป็นคนที่เพิ่งเริ่มใช้ Go หรือยังอยู่ระดับต้น
      คนรอบตัวชอบการจัดการข้อผิดพลาดที่ “ยืดยาว” แต่ชัดเจน ตรงไปตรงมา และอ่านง่าย
    • ถ้าจะลองแย้งอีกด้าน IDE กับฟอนต์อาจทำให้ในโหมดไวยากรณ์ Go เท่านั้น if err != nil ถูกไฮไลต์หรือเรนเดอร์เป็นพื้นหลังแบบจาง ๆ เหมือน ligature ขนาดเล็กตัวเดียวได้
      แบบนั้นรูปแบบที่ต่างจากสตริงนี้เป๊ะ ๆ เช่น if err == nil จะเด่นขึ้นมาแทน
    • เป็นประเด็นที่ดี ดูเหมือนอาจแก้ได้ด้วยสัญลักษณ์แบบพับในเอดิเตอร์ เช่น if err … {
  • ชอบการจัดการข้อผิดพลาดแบบชัดเจนของ Go
    ฟังก์ชันอาจสำเร็จเสมอ หรืออาจสำเร็จหรือล้มเหลวก็ได้ ฟังก์ชันที่สำเร็จเสมอนั้นเรียบง่าย ส่วนฟังก์ชันที่ล้มเหลวได้ หากล้มเหลว โค้ดภายนอกก็ไม่สามารถเดินหน้าต่อในสถานะล้มเหลวได้ จึงต้องจัดการ
    ตรงนี้เองที่ภาษาต่าง ๆ แยกแนวทางกัน หลายภาษาจะโยน exception ขึ้นไปจนกว่าจะมีใครจับอย่างชัดเจน และให้ stack trace บางรูปแบบ
    ใน Go สิ่งที่ชอบคือเวลาเขียนโค้ดจะมีตัวเลือกที่ต้องเลือกเสมอ: เพิกเฉยข้อผิดพลาดแล้วไปต่อ (foo, _ := doSomething()), คืนค่าก่อนโดยไม่มีข้อมูลที่มีความหมาย (return nil, err), คืนค่าก่อนพร้อมแนบบริบทที่มีประโยชน์, หรือตีความข้อผิดพลาดที่ได้รับแล้วแตกแขนง
    เช่น หากฐานข้อมูลไม่พบแถวที่ต้องแก้ไข ชั้นบริการอาจคืนข้อผิดพลาด not found แล้ว API แปลงเป็น 404 หรือฟังก์ชันลบแบบ idempotent อาจตีความ not found ว่าเป็นความสำเร็จก็ได้
    ถ้าเป็น Go 2 หรือภาษาอื่น ก็อยากให้มี Result type แบบ Rust/Swift แทนทูเพิลที่อาจเป็น nil ได้ และมีชนิดข้อผิดพลาดที่พิมพ์ชนิดได้ดีกว่าและ enumerate ได้ แทนการใช้ error โดยตรงเสมอ
    แต่ถ้าเพิ่ม Result เข้าไปบนการคืนค่าทูเพิลตาม idiom ของ Go 1 ก็จะเกิดหลายวิธีในการทำสิ่งเดียวกัน ทำให้สับสนและแตกแยก จึงเหมาะกับ Go 2 หรือภาษาใหม่มากกว่า

    • จากประสบการณ์ของผม นโยบายการจัดการข้อผิดพลาดควรถูกมอบให้ผู้เรียก
      ชั้นล่าง ๆ ของสแตกโดยทั่วไปไม่รู้ว่าควรทำอะไร จึงไม่ควรจัดการข้อผิดพลาด
      นโยบายที่เรียกว่าการจัดการข้อผิดพลาด สุดท้ายมักกลายเป็นการห่อข้อผิดพลาดแล้วส่งกลับขึ้นไปบนสแตกอีกที และกลายเป็นงานจุกจิกไม่น้อย
    • “ถ้าฟังก์ชันล้มเหลว ก็ต้องจัดการความล้มเหลวนั้น” นี่แหละคือจุดที่ Go ล้มเหลว
      Go ปล่อยให้เพิกเฉยข้อผิดพลาดได้โดยสมบูรณ์ และผลลัพธ์อาจนำไปสู่การแครชได้
      ทั้งที่ชี้ข้อกำหนดสำหรับการสร้างซอฟต์แวร์ที่แข็งแรงได้ตรงจุด แต่กลับชอบวิธีจัดการข้อผิดพลาดของ Go นั้นออกจะเข้าใจยาก
    • อยากให้ไวยากรณ์ของ borgo[1] กลายเป็นภาษา Go 2 ไปเลย ฝันไว้ก็ไม่เสียหาย
      [1]: https://borgo-lang.github.io/ | https://github.com/borgo-lang/borgo
    • ด้วยความเร็วแบบนี้ Go2 ดูเหมือนห้องทดลองไอเดียที่จะไม่มีวันออกจริง
    • อยากถามว่าบอกว่าดีเมื่อเทียบกับอะไร
      ภาษา functional ทั้งหมด, ภาษาสมัยใหม่จำนวนมากอย่าง Rust, แม้แต่ Java ที่มี checked exception ก็ให้สิ่งนี้ได้
      ถ้าเป็นภาษาที่มี generics ก็มักจะจำลอง “การจัดการข้อผิดพลาด” แบบ Go ได้ และอาจได้โค้ดที่ดีกว่าด้วย
      ถ้าคำตอบคือ JavaScript หรือ Python นั่นก็เป็นแพตเทิร์นการเปรียบเทียบที่พบบ่อยอยู่
  • สำหรับ Go นี่เป็นการตัดสินใจที่ถูกต้อง ตอนเริ่มใช้ Go แรก ๆ ไม่ชอบการจัดการข้อผิดพลาด แต่ตอนนี้ชอบมากจริง ๆ
    จุดเปลี่ยนมีสองอย่าง คือได้อ่านบทความ https://go.dev/blog/errors-are-values แล้วรับมุมมอง “ข้อผิดพลาดคือค่า” อย่างจริงจัง และจากฐานคิดนั้นก็ได้สร้างแพ็กเกจที่ค่อนข้างได้รับความนิยมอย่าง https://github.com/stytchauth/sqx ด้วย
    อีกอย่างคือเริ่มคุ้นเคยกับการใช้ panic(err) ทีละน้อยในสถานะผิดพลาดที่ไม่สมเหตุสมผลจริง ๆ
    ไม่มีเหตุผลที่จะบังคับให้โค้ดแม่ต้องจัดการสถานะแปลก ๆ ทั้งหมดที่มันไม่มีทางรู้สึกได้ว่าควรจัดการอย่างไร และ panic ที่วางดี ๆ สักหนึ่งสองจุดก็สามารถตัดการตรวจข้อผิดพลาดหลายร้อยจุดออกจาก codebase ได้
    ตัวอย่างเช่น ลองคิดถึงปัญหาอย่างการมี logger เริ่มต้นอยู่ใน ctx หรือไม่

    • เป็นเรื่องน่าเสียดาย เหตุผลทั้งสองข้อที่ยกมาไม่เกี่ยวกับว่าการจัดการข้อผิดพลาดของ Go แย่แค่ไหนเลย และการทำให้การจัดการข้อผิดพลาดดีขึ้นก็ไม่ได้ทำให้สิ่งเหล่านั้นแย่ลง
      กลับมีโอกาสจะดีขึ้นด้วยซ้ำ
    • แม้แต่ PHP ก็ยังมีการจัดการข้อผิดพลาดที่ดีกว่า ด้วยระดับข้อผิดพลาดและ ตัวดำเนินการ @ สำหรับกดข้อผิดพลาดที่จุดเรียก
      bash ก็มี -e
    • ผมก็ชอบแนวทางของ Go ถ้าแลกกับการรู้ได้แน่ชัดขึ้นว่าเกิดอะไรขึ้น ผมยอมมีจำนวนบรรทัดโค้ดมากขึ้น
      สมัยเริ่มใช้ C# ใหม่ ๆ เคยคิดว่าการเข้าใจ flow ของ try/catch/finally, using, การซ้อนกัน, ถ้าเกิดข้อผิดพลาดใน catch จะเป็นอย่างไร, ถ้าเกิดใน finally จะเป็นอย่างไร เป็นเรื่องที่ฉลาด
      ตอนนี้คิดว่าไม่ต้องคิดเรื่องพวกนั้นดีกว่า
    • sum type error แบบ Rust ก็เป็นค่าเช่นกัน
  • ผมไม่ชอบวิธีที่บทความนี้พูดเหมือนว่าปัญหาหลักของการจัดการข้อผิดพลาดใน Go คือไวยากรณ์ที่เยิ่นเย้อเกินไป เรื่องนั้นผมไม่ได้ใส่ใจเท่าไร
    สิ่งที่สำคัญกว่าคือข้อผิดพลาดอาจถูกทิ้งไปเงียบ ๆ หรือถูกละเลยโดยไม่ตั้งใจได้, ผลลัพธ์จากการเรียกฟังก์ชันไม่ใช่ค่า จึงเก็บหรือส่งต่อได้ไม่ง่าย, ต้องใช้ errors.Is และข้อผิดพลาดแบบ “ซ้อนกัน” ทั้งชุดเป็นกลไก runtime แปลก ๆ ที่ไม่ค่อยเข้ากับ type system
    การ switch กับข้อผิดพลาดก็ทำได้ยาก, standard library ใช้ค่า sentinel และการทำงานร่วมกับ generics ก็ไม่ดี จนต้องมีแพ็กเกจอย่าง errgroup
    ยังมีอะไรที่ผมตกหล่นไปอีกไหม?

    • 90% ของเวลาที่ทำงานกับ Go แบบมืออาชีพคือ การฝืนสร้าง test case เพื่อให้แต่ละแขนงที่ return ข้อผิดพลาดถูกครอบคลุมด้วย statement coverage
      ถ้าเป็นภาษาที่มี exception คงไม่มีใครทำแบบนั้น
    • ผมไม่เห็นว่าบทความนี้มีตรงไหนที่อ้างว่า “ปัญหาหลักคือไวยากรณ์ที่เยิ่นเย้อเกินไป”
      พวกเขาตัดสินใจว่าจะไม่พยายามเปลี่ยนไวยากรณ์การจัดการข้อผิดพลาดอีกในอนาคตอันคาดการณ์ได้ และนั่นทำให้มีพื้นที่ความสนใจไปดูปัญหาอื่น ๆ ไม่ว่าจะเป็นเรื่องข้อผิดพลาดหรือหัวข้ออื่น
    • ต้องจำไว้ว่าแม้แต่กว่าที่ Go จะรองรับ generics ในรูปแบบใดรูปแบบหนึ่งก็ใช้เวลานานมาก
      วิวัฒนาการของ Go ช้าเหมือนธารน้ำแข็ง และสำหรับหลายคน นั่นไม่ใช่บั๊กแต่เป็นฟีเจอร์
    • เห็นด้วย 100% ทั้งสองคนเป็น Googler ด้วย ยิ่งน่าเสียดายที่ต้องผิดหวังกับทีม Go อีกครั้ง
    • เห็นด้วยกับข้อ 1 แต่บรรเทาได้ระดับหนึ่งด้วยเครื่องมือพัฒนาอย่าง errcheck: https://github.com/kisielk/errcheck?tab=readme-ov-file
  • คำอธิบายทำนองว่า “ความเห็นในแบบสำรวจที่บอกว่าข้อผิดพลาดไม่มี stack trace แก้ได้โดยให้ฟังก์ชันช่วยสร้างและคืนข้อผิดพลาดที่เสริมข้อมูลแล้ว” นั้นตลกดี
    การ ใส่ stack trace ด้วยมือ อย่าง if err != nil { return fmt.Errorf("invalid integer: %q", a) } แล้วเรียกว่า “การจัดการข้อผิดพลาด” นี่น่าสนุกดี
    ตามนิยามของทีม Go exception ก็คงเป็นการจัดการข้อผิดพลาดให้อัตโนมัติแล้ว แน่นอนว่ายกเว้นภาษา C++

    • น่าขำที่คนเห็น stack trace เต็มหน้าจอแล้วบอกว่าชัดเจนและมีประโยชน์
      มันอาจเป็นอย่างนั้นก็ได้ แต่จำเป็นต้องมีทั้งหมดนั้นจริงหรือ? แล้วค่าใช้จ่ายของ log ล่ะ?
      ผมว่า error ที่ wrap มาเป็นบรรทัดเดียวและตัด noise จาก framework กับ runtime ออกไปนั้นดีกว่ามาก
      ถ้า wrap ดี ๆ ก็ค้นหาได้ง่ายมาก และโดยปกติตามรอยได้มีประสิทธิภาพกว่า stack trace
      ผมใช้ Go แบบเต็มเวลามากว่า 10 ปี และไม่เคยมีสักครั้งที่ต้องการ noise ยืดยาวจากฟังก์ชัน runtime หรือ call stack
  • จากมุมมองของนักพัฒนา Elixir นี่ดูเหมือนเรื่องบ้าบอ
    ใน Erlang/Elixir ปกติแก้ด้วยการให้ฟังก์ชันคืน tuple แบบ {:ok, result} หรือ {:error, description_or_struct}
    พอใช้คำสั่ง with ของ Elixir ร่วมด้วย ก็สามารถรวบการจัดการข้อผิดพลาดไว้ด้านล่าง ทำให้อ่านง่ายขึ้นมาก
    Go ก็น่าจะเพิ่มสิ่งที่เทียบเท่ากับ with clause ให้ฟังก์ชันต่าง ๆ ดำเนินต่อไปตราบใดที่ข้อผิดพลาดเป็น nil แล้วมี clause จัดการข้อผิดพลาดอยู่ด้านล่าง

    • จากหลักฐานที่มี ดูเหมือนว่า Go ไม่มีทางนำคำสั่ง with มาใช้เลย
      น่าสนใจที่ Go เลื่อนโครงสร้างพื้นฐานและมีคุณค่าอย่างชัดเจน เช่น generics, error handling, package management ออกไปนานมากเพราะ ขาดฉันทามติในชุมชน
      generics ใช้เวลา 13 ปีหลังเปิดเป็นโอเพนซอร์ส, ผ่านมา 16 ปีแล้วตอนนี้ก็ยังไม่มี error handling และ package management ใช้เวลาราว 9 ปี
      การไตร่ตรองมีคุณค่า แต่การปล่อยของก็มีคุณค่าเช่นกัน คนที่เขียนคอมเมนต์ GitHub 900 ความเห็นสุดท้ายก็ยังจะใช้ Go ต่อไป และมีความเป็นไปได้ว่าสิ่งที่ถูกใส่เข้าไปในภาษาจะดีกว่าการผัดผ่อนไปเรื่อย ๆ
    • multiple return ของ Go เองก็ดูแปลกในมุมมองของผม
      ฟังก์ชันที่มี return type หลายตัว แทบทำอะไรไม่ได้นอกจาก assign ให้ตัวแปร
    • ผู้ใช้ Haskell กับแฟน ๆ Rust มองว่า sum type เป็นของพวกเขา และผู้คนอ่านคอมเมนต์กับบทความของพวกเขาแล้วเชื่อ จากนั้นก็ไม่อยากตกลงไปในโพรงกระต่าย Hindley-Milner ที่น่ากลัว เลยพากันหลีกเลี่ยง sum type
      แต่ใน Erlang และ Elixir มันเป็นแนวทางที่เป็น idiomatic อย่างสมบูรณ์โดยไม่มีภาระใด ๆ
      ที่จริงยังทรงพลังกว่าตระกูล ML มากด้วยซ้ำ เพราะ sum type ของฝั่งนั้นเป็นแบบเปิด
  • ผมไม่ได้ตามการถกเถียงนี้อย่างละเอียด แต่ไม่เข้าใจว่าทำไมไม่เอาวิธีแบบ Rust มาใช้ตรง ๆ
    หลังจาก Go มี generics วิธีที่ผมเพิ่มเข้าไปทันทีเองก็เป็นแบบนั้น
    ในบทความที่ลิงก์ไว้ เห็นมีแค่คำอธิบายว่า “Rust ไม่มีสิ่งที่เทียบเท่า handle และความสะดวกของ operator ? อาจทำให้ละเว้นการจัดการที่เหมาะสมได้”
    แต่ผมไม่เข้าใจว่าความสะดวกหมายถึงการเมิน error ได้อย่างไร
    ปัญหาครึ่งหนึ่งของวิธีแบบ Go คือมันไม่บังคับอะไรกับผลลัพธ์เลย และบังคับตรวจ error แค่ขั้นต่ำเท่านั้น
    x, err := strconv.Atoi("123"); fmt.Println("result:", x) จะเจอ declared and not used: err แต่หลังจากแปลงค่าครั้งที่สอง ต่อให้ไม่ตรวจ err ก็อาจรันผ่านได้โดยไม่รู้ว่ามีปัญหา เพราะค่า default ของ y เป็น 0
    ถ้าเขียนว่างไว้แบบ if err != nil { } ก็ยัง compile และรันได้ โดยไม่รู้ว่ามีอะไรผิดพลาด
    ถ้าทำให้ค่าที่คืนกลับมาเป็น Result ก็จะถูกบังคับให้ต้องตัดสินใจ ต่อให้บางคนใช้ ! มั่ว ๆ หรือใช้ ? โยนขึ้นไปแบบง่าย ๆ โดยไม่จัดการเคส error แล้วอย่างนั้นจะห้าม panic ด้วยไหม?

    • Go ไม่มี sum type จึงมี Result ไม่ได้
      และเพราะความยึดติดแปลก ๆ ว่าทุก type ต้องมี zero value ที่กำหนดไว้ เลยเพิ่ม sum type ไม่ได้ด้วย
    • ผมเข้าใจว่าถ้า ? ใช้ง่าย ก็หมายความว่าจะไม่มีใคร wrap error อีกต่อไป
      เป็นตรรกะที่น่าสงสัยมาก
      ตั้งแต่แรกก็ออกแบบให้ ? ส่งเสริมการ wrap error ได้อยู่แล้ว
    • เพราะไม่ชัดเจนว่าเวลานำแนวทางแบบ Rust มาใส่ใน Go รูปแบบที่เทียบเท่ากันเป๊ะ ๆ คืออะไร
      ตัวอย่างเช่น สิ่งที่เทียบกับ From ของ Rust ใน Go ควรมีหน้าตาอย่างไร?
    • ? มองเห็นได้ไม่เด่น และซ่อนการแตกแขนงของ control flow ไว้ใน statement หรือ expression เดียว
      นั่นก็เป็นเหตุผลหนึ่งที่ Go ไม่มี ternary operator และเลือกใช้ statement if ที่แต่ละ branch อยู่คนละบรรทัด
      ตั้ง breakpoint ก็ไม่ง่าย และทำให้เอนเอียงไปทางส่ง error ขึ้นไปตรง ๆ มากกว่าจะเสริมหรือจัดการ error
    • ผมคิดว่า := คือการประกาศและกำหนดค่าใน statement เดียว แต่ในบรรทัดที่ 5 ของตัวอย่าง มีการประกาศ err อีกครั้ง แล้ว err ตัวใหม่ไม่ได้บัง err เดิมอยู่หรือ?
      ถ้าอย่างนั้นก็น่าจะ fail ด้วย declared and not used: err เพราะตัวแปร err ใหม่ไม่ได้ถูกใช้
      หรือถ้ามีตัวแปรอยู่แล้ว := จะทำตัวเหมือนการ assign ปกติไปเลย?
  • คำพูดที่ว่า “การที่ error ไม่มี stack trace แก้ได้ด้วยการให้ helper function สร้างและคืน error ที่เสริมข้อมูลแล้ว” มองโลกจริงในแง่ดีเกินไป
    ภาษาที่มี stack trace ให้สิ่งนี้มาฟรี ๆ แต่ใน Go ต้อง implement เองทุกครั้ง
    ตัวคุณเองอาจเป็นนักพัฒนาที่มีวินัยและใส่รายละเอียดเพิ่มเสมอ แต่ไม่ได้แปลว่าสมาชิกทีมทุกคนจะมีวินัยแบบเดียวกัน
    สิ่งที่ดีที่สุดของ stack trace คือมันให้ call path ไปจนถึง error
    ถ้าเกิด error ใน method ที่ถูกเรียกจากหลายที่ stack trace จะบอกได้ทันทีว่าเข้ามาทางเส้นทางไหน
    ผมทำงานแนว sysadmin/SRE มาหลายปีและแก้ปัญหามามากมาย เวลามี stack trace ปัญหาง่าย ๆ ที่ต้นเหตุชัดเจนมักจบใน 1–2 นาที
    แต่ใน Go ถ้ามีใครไม่เสริมข้อมูลให้ error หรือ reuse ข้อความ error เดิม ปัญหาง่าย ๆ ก็กลายเป็นงานสืบสวนและใช้เวลานานขึ้น