- Go 1.22 เปลี่ยนตัวแปรของลูป
for จากขอบเขตที่ครอบคลุมทั้งลูปเป็น ขอบเขตต่อรอบการวนซ้ำ เพื่อลดความผิดพลาดคลาสสิกของ Go ที่ closure จับตัวแปรเดียวกันผิดพลาด
- ในความหมายเดิม แม้ไม่มี goroutine ฟังก์ชันที่ทำงานหลังจบการวนซ้ำก็อาจอ้างถึง
v หรือ i ตัวเดิม ทำให้เห็นแค่ค่าท้ายสุดหรือทำให้การทดสอบ ผ่านแบบผิดๆ
- ตัววิเคราะห์
loopclosure ของ go vet และ gopls จับได้เฉพาะกรณีที่แน่ชัด จึงเกิด false negative ได้ ส่วนตัวตรวจที่รุกมากกว่าจะเพิ่มโค้ด x:= x ที่ไม่จำเป็นเพราะ false positive
- ความหมายใหม่จะใช้กับโมดูลที่ประกาศ
go 1.22 ขึ้นไปใน go.mod เท่านั้น และใน Go 1.21 สามารถลอง พรีวิว ได้ด้วย GOEXPERIMENT=loopvar
- Google บังคับใช้โหมดนี้กับทุกบิลด์ใน toolchain ภายในตั้งแต่ต้นเดือนพฤษภาคม 2023 และตลอด 4 เดือนก็ไม่มีรายงานปัญหาในโปรดักชัน แต่พบการทดสอบที่เขียนผิด
กับดักการจับตัวแปรของลูป for แบบเดิม
- ตัวแปรลูป
for แบบเดิมของ Go มี ขอบเขตครอบคลุมทั้งลูป ดังนั้นโค้ดที่อ้างถึงตัวแปรนั้นหลังจบการวนซ้ำอาจเห็นค่าที่ต่างจากที่ตั้งใจ
- หากวน
values := []string{"a", "b", "c"} แล้วสร้าง goroutine 3 ตัว แต่ละ goroutine จะพิมพ์ตัวแปร v ตัวเดียวกัน ไม่ใช่ v ของแต่ละรอบ
- ปัญหาเดียวกันเกิดขึ้นได้ แม้ไม่มีงานพร้อมกัน
- ถ้าเก็บ
func() { fmt.Println(i) } ลงใน slice ภายในลูปแล้วค่อยนำไปรันทีหลัง แต่ละฟังก์ชันก็จะอ้างถึง i ตัวเดียวกัน ไม่ใช่ค่าของแต่ละรอบ
ปัญหาในโปรดักชันและข้อจำกัดของตัววิเคราะห์
- ความผิดพลาดลักษณะนี้นำไปสู่ปัญหาในโปรดักชันของหลายบริษัท และ issue สาธารณะของ Let’s Encrypt ก็เป็นหนึ่งในนั้น
- ในกรณีของ Let’s Encrypt ระหว่างการวน map ได้คัดลอก
k ด้วย kCopy := k แล้ว แต่ modelToAuthzPB(&v) ใช้ pointer ไปยังฟิลด์ของ v ระหว่างสร้างผลลัพธ์ จึงจำเป็นต้องคัดลอก v แยกด้วย
- การจับตัวแปรกินข้ามหลายฟังก์ชัน ทำให้สังเกตปัญหาได้ยาก
- เครื่องมือวิเคราะห์แบบ static ตัดสินได้ยากว่าตัวแปรจะมีชีวิตอยู่ต่อหลังการวนซ้ำหรือไม่ จึงต้องประนีประนอมระหว่าง false positive และ false negative
- ตัววิเคราะห์
loopclosure ของ go vet และ gopls รายงานเฉพาะปัญหาที่แน่ชัด จึงยอมรับ false negative
- ตัวตรวจที่รุกมากกว่าอาจชี้โค้ดที่ถูกต้องว่าเป็นโค้ดผิด
- เมื่อดู commit ในโค้ด Go แบบโอเพนซอร์สที่เพิ่มบรรทัด
x := x จะพบว่ามีทั้งการแก้บั๊กจริงและการเปลี่ยนแปลงที่ไม่จำเป็นปะปนกันมาก
- มีสถานการณ์ที่นักพัฒนาต้องเพิ่มโค้ดที่ไม่จำเป็นเพื่อให้ผ่านตัวตรวจ
- ใน diff สองแบบอย่าง
informer := informer และ a := a มีเพียงอันเดียวที่เป็นการแก้บั๊ก ส่วนอีกอันเป็นการเปลี่ยนแปลงที่ไม่จำเป็น แต่ถ้าไม่รู้ข้อมูลชนิดและฟังก์ชันก็แยกได้ยาก
ความหมายใหม่ของลูปใน Go 1.22
- ใน Go 1.22 ตัวแปรของลูป
for จะถูกเปลี่ยนให้มี ขอบเขตแยกสำหรับแต่ละรอบการวนซ้ำ
- ตัวอย่างก่อนหน้านี้จะไม่ถือเป็นโปรแกรม Go ที่มีบั๊กอีกต่อไป และทั้งปัญหาในโปรดักชันจากความผิดพลาดลักษณะนี้รวมถึงความจำเป็นของเครื่องมือตรวจที่ไม่แม่นยำก็จะลดลง
- เพื่อคงความเข้ากันได้แบบย้อนหลัง ความหมายใหม่นี้จะใช้เฉพาะกับแพ็กเกจในโมดูลที่ประกาศ
go 1.22 ขึ้นไปใน go.mod
- จึงสามารถย้ายแบบค่อยเป็นค่อยไปได้โดยไม่ต้องเปลี่ยนทั้งโค้ดเบสในครั้งเดียว
- และยังควบคุมระดับไฟล์ได้ด้วยบรรทัด
//go:build
- โค้ดเดิมจะยังคงความหมายปัจจุบันไว้เหมือนเดิม
- การเปลี่ยนแปลงจะมีผลเฉพาะกับโค้ดใหม่หรือโค้ดที่อัปเดตแล้ว
- นักพัฒนาสามารถควบคุมได้ว่าแพ็กเกจใดจะเปลี่ยนไปใช้ความหมายใหม่นี้เมื่อใด
กลไกความปลอดภัยใน Go เวอร์ชันก่อนหน้า
- ตามงานด้าน forward compatibility ของ Go นั้น Go 1.21 จะไม่คอมไพล์โค้ดที่ประกาศ
go 1.22 ขึ้นไป
- ใน point release ของ Go 1.20.8 และ Go 1.19.13 ก็มีการใส่การจัดการพิเศษเพื่อให้ได้ผลแบบเดียวกัน
- หลังจาก Go 1.22 ออกแล้ว โค้ดที่เขียนโดยอาศัยความหมายใหม่จะไม่ถูกคอมไพล์ด้วยความหมายเดิมอีก เว้นแต่จะใช้ Go เวอร์ชันที่หมดการสนับสนุนแล้ว ที่เก่ามาก
ทดลองพรีวิวใน Go 1.21
- Go 1.21 มี พรีวิว ของการเปลี่ยนขอบเขตลูปนี้มาให้แล้ว
- หากคอมไพล์โดยตั้งค่า
GOEXPERIMENT=loopvar จะละเลยบรรทัด go ใน go.mod และใช้ความหมายใหม่กับทุกลูป
- หากต้องการตรวจสอบว่าแพ็กเกจและ dependency ทั้งหมดผ่านการทดสอบภายใต้ความหมายใหม่ของลูปหรือไม่ ให้รันดังนี้
GOEXPERIMENT=loopvar go test
- ใน Go Playground สามารถทดลองความหมายใหม่ได้โดยใส่คอมเมนต์
// GOEXPERIMENT=loopvar ไว้บนสุดของโปรแกรม
- toolchain Go ภายในของ Google ถูกแพตช์ให้บังคับใช้โหมดนี้กับทุกบิลด์ตั้งแต่ต้นเดือนพฤษภาคม 2023 และหลังจากนั้นตลอด 4 เดือนก็ไม่มีรายงานปัญหาในโค้ดโปรดักชัน
บั๊กในเทสต์ที่ความหมายใหม่เปิดเผยออกมา
- ความหมายใหม่ของลูปไม่ได้ทำให้เกิดปัญหาในโค้ดโปรดักชัน แต่ได้เปิดเผย การทดสอบที่ผ่านแบบผิดๆ
- ในตัวอย่าง subtest ที่ใช้
t.Parallel นั้น Go 1.21 จะกันแต่ละ subtest ไว้จนกว่าทั้งลูปจะจบก่อน แล้วจึงค่อยรันแบบขนาน
- เมื่อจบลูปแล้ว
v จะเป็น 6 เสมอ ดังนั้นทุก subtest จึงตรวจว่า 6 เป็นเลขคู่และผ่าน
- แต่ในเคสทดสอบจริงมีค่า
1 อยู่ด้วย ดังนั้นการทดสอบควรล้มเหลว
- ใน Go 1.21 มีการปรับปรุงความแม่นยำของตัววิเคราะห์
loopclosure จึงสามารถระบุและรายงานปัญหานี้ได้
- ตัวอย่างรายงานใน Go Playground: ตัวอย่างโปรแกรม
- หาก
go vet รายงานปัญหาลักษณะนี้ในเทสต์ การแก้ไขจะช่วยเตรียมพร้อมสำหรับ Go 1.22
- เครื่องมือและตัวอย่างสำหรับค้นหาว่าลูปใดทำให้เทสต์บางตัวล้มเหลวเมื่อใช้ความหมายใหม่ ได้ถูกรวบรวมไว้ใน FAQ
อ่านเพิ่มเติม
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
อาจมีตัวอย่างที่เก่ากว่านี้มากก็ได้ แต่คำเตือนที่เก่าแก่ที่สุดเกี่ยวกับพฤติกรรมนี้ที่หาเจอจากการค้นหา 60 วินาทีคือ comp.lang.lisp FAQ ที่โพสต์ไว้เมื่อปี 1992 หรือกว่า 30 ปีก่อน
มีการอธิบายว่า
DOTIMES,DOLIST,DOใช้การกำหนดค่าแทนการ bind ตอนอัปเดตตัวแปรที่ใช้วนซ้ำ ดังนั้นถ้าlambdaจับnเหมือนในตัวอย่าง closure ทั้ง 10 ตัวจะถูกสร้างขึ้นบนค่าของตัวแปรNตัวเดียวกันถ้าจับแบบอ้างอิง จริง ๆ แล้วก็เป็นพฤติกรรมที่คาดไว้
ถึงอย่างนั้น พอเรียนรู้วิธีทำงานสักครั้งแล้วก็ไม่ใช่ปัญหา และถ้าจำเป็นก็เลือกฟอร์มแล้ว macro expand เพื่อตรวจสอบวิธี implement ได้
ทีมภาษา C# ก็เจอปัญหาเดียวกันหลังจากนำ lightweight closure เข้ามาใน C# 4.0 และไม่นานก็เห็นชัดว่านี่เป็นกับดัก
ผู้ใช้แทบจะใช้ตัวแปรลูปผิดอยู่เสมอ และใน C# 5.0 ก็ใส่การเปลี่ยนแปลงที่ทำให้เสีย compatibility เข้าไป
Eric Lippert เขียนบทความที่อธิบาย “ทำไม” จากมุมนั้นได้ดี: https://ericlippert.com/2009/11/12/closing-over-the-loop-var...
บทความประกาศ C# 5 เดิมหายากแล้ว หวังว่าจะไม่สูญหายไประหว่างการย้ายบล็อกหลายรอบบนโดเมนของ Microsoft หลังปี 2012
ถ้านึกถึงความวุ่นวายตอนเปลี่ยนจาก Python 2 เป็น 3 แค่เรื่องการเปลี่ยนชนิดสตริง ก็ไม่คิดว่าการเปลี่ยนแปลงนี้จะเข้าได้ก่อน Python 4.0
แล้วก็คงมีใครสักคนบ่นว่า Python แย่เพราะไม่แก้เรื่องแบบนี้ แล้วก็หันมาด่า Python อีกทีเพราะสคริปต์ที่เขียนไว้เมื่อปี 2003 รันไม่ได้
ผมคิดว่าเขามีบทบาทสำคัญในการช่วยให้ข้อเสนอเปลี่ยนภาษาแบบนี้ข้ามกำแพง “ปฏิเสธไว้ก่อน” ที่ควรมีโดยพื้นฐานได้
อีกจุดที่โน้มน้าวได้มากคือผลจากการสแกนโค้ดเบสโอเพนซอร์ส เพื่อดูสมดุลระหว่างบั๊กที่จะถูกแก้กับบั๊กใหม่ที่จะเกิดขึ้น
เพราะเป็นการส่งค่า จึงจับสถานะของตัวแปร ณ เวลาที่เรียก ช่วยลดความกำกวมของโค้ด
ถ้าพยายามจับตัวแปรแบบแปลก ๆ เช่น collection ที่ใช้สะสมเพื่อแปลง array เป็น map กับตัวแปรที่ประกาศไว้จะทำงานต่างกัน
Go ดูเหมือนพยายามสร้างสมดุลโดยใช้พฤติกรรมนี้เฉพาะกับตัวนับลูป แต่ตัวแปรบางตัวยังคงทำงานแปลกอยู่
โดยเฉพาะกรณีที่นิยามตัวแปรลูปหลายตัวเพื่อสแกนอินพุตโดยตรง ผมสงสัยว่าจะเกิดอะไรขึ้น
for(let)เข้ามาhttps://eli.thegreenplace.net/2019/go-internals-capturing-lo... ดูเหมือนจะอธิบายปัญหานี้ละเอียดกว่า
i := iแบบเก่าทำงานได้นั้นต่างจากที่ผมคิดไว้โดยสิ้นเชิงตอนแรกผมคิดว่า
iตัวใหม่ถูกส่งเข้า goroutine ดังนั้น escape analysis จะทำเครื่องหมายว่ามัน escape ออกนอก lexical scope จึงถูกจัดสรรบน heap และแต่ละรอบจะมีการจัดสรร heap หนึ่งครั้ง ทำให้ goroutine แต่ละตัวอ้างถึงตำแหน่งหน่วยความจำของตัวเองจริง ๆ แล้วคอมไพเลอร์ Go มี heuristic สำหรับเลือก capture by reference หรือ capture by value และมีเงื่อนไขว่าค่าที่ไม่ถูกอัปเดตหลัง initialization จะถูก capture by value
iตัวใหม่อยู่ใน scope ของ body ของforและตัวลูปเองไม่ได้อัปเดตมัน จึงถูกตัดสินว่าเป็นค่าที่ไม่ถูกอัปเดตหลัง initialization และสร้างโค้ดที่ capture by value โดยไม่ต้องจัดสรร heapผมรู้ว่าแบบหลังดีกว่า แต่ก็อยากฟังจากคนที่รู้ Go ลึก ๆ ว่าทำไมแบบแรกถึงไม่เกิดขึ้นพร้อมกัน
การเปลี่ยนแปลงนี้จะไม่ทำให้โปรแกรมที่พึ่งพาพฤติกรรมปัจจุบันพังหรือ?
go 1.22ขึ้นไปในgo.modเท่านั้นในระดับไฟล์ก็สามารถตัดสินใจได้โดยใช้บรรทัด
//go:buildคำมั่นนั้นบอกว่าโปรแกรมที่เขียนตามสเปก Go 1 จะต้องคอมไพล์และทำงานได้อย่างถูกต้องต่อไปโดยไม่ต้องเปลี่ยนแปลงตลอดอายุของสเปก และถึงแม้สักวันอาจมีสเปก Go 2 ออกมา แต่ก่อนถึงตอนนั้น โปรแกรม Go ที่ทำงานได้ในวันนี้ก็ควรทำงานต่อไปได้แม้ใน point release อย่าง Go 1.1, Go 1.2
พวกเขาคิดว่าจำนวนคนที่เจอบั๊กโดยไม่ตั้งใจจากดีไซน์นี้ น่าจะมากกว่าจำนวนคนที่จะได้รับผลกระทบจากการแก้ไขนี้มาก
เท่าที่จำได้ เขาบอกว่าแทบไม่มีกรณีใน codebase ของ Google หรือโค้ดบน GitHub ที่การเปลี่ยนแปลงนี้ทำลายพฤติกรรมที่คาดหวัง
หลังจากตรวจสอบแล้วว่า codebase ที่ได้รับผลกระทบมีน้อยแค่ไหน และสร้างกลไกให้ต้องแก้โค้ดเชิงรุกเพื่อใช้พฤติกรรมใหม่ผ่านการระบุเวอร์ชันใน
go.modแล้ว จึงตัดสินใจทำลายความเข้ากันได้ย้อนหลังhttps://twitter.com/go100and1/status/1690412229135601664
https://twitter.com/go100and1/status/1690587305806057472
https://twitter.com/go100and1/status/1690589791686119424
https://twitter.com/go100and1/status/1690591234715492352
https://twitter.com/go100and1/status/1690593184857145344
https://twitter.com/go100and1/status/1691456732151889920
ส่วนใหญ่ไม่ได้ถูกกล่าวถึงในเอกสารข้อเสนอเลย
ใน Python ก็เคยเจอปัญหานี้เหมือนกัน แต่ไม่ใช่ช่วงหลัง ๆ
ไม่แน่ใจว่า Python เปลี่ยนไปแล้ว หรือเป็นเพราะผมเริ่มสังเกตเห็นปัญหาเอง
แค่โค้ดนี้ก็เพียงพอจะเห็นว่ายังอาจเป็นปัญหาใน Python ได้:
funcs = [(lambda: x) for x in range(3)];funcs[0]()จะพิมพ์2เมื่อก่อน Python แย่กว่านี้อีก และแชร์ scope ไปถึงนอก list comprehension ด้วย
เมื่อใช้ lambda ใน list comprehension หรือในลูป มันจะจับ reference ไปยังตัวแปร
xไม่ใช่ค่าปัจจุบันของxตอนเรียก
funcs[0]()นั้นxถูกตั้งค่าเป็นค่าตัวสุดท้ายของrangeคือ2ไปแล้วถ้าต้องการพฤติกรรมที่ต้องการ ให้ส่ง
xเป็นอาร์กิวเมนต์ค่าเริ่มต้นของ lambda:funcs = [(lambda x=x: x) for x in range(3)]ผมเคยใช้ Go แค่นิดหน่อย และรู้ปัญหาทั่วไปที่การเปลี่ยนแปลงนี้แก้ แต่ยังไม่ค่อยเข้าใจตัวอย่างที่ละเอียดกว่าอย่างกรณี letsencrypt หรือ
"range c.informerMap"เทียบกับ"range alarms"ใน
for k, v := range someMapนั้นvเป็นชนิดค่าของ map และมี binding เดียวตลอดทั้งลูปที่ถูกคัดลอกในแต่ละรอบใช่ไหม? ถ้าเป็นแบบนั้นก็อธิบายปัญหาได้ แต่ผมคาดว่าvน่าจะเป็น reference ที่ชี้เข้าไปใน mapลองไล่อ่าน “For statements with range clause” ในสเปกอย่างเร็ว ๆ แล้วก็ไม่เจอคำตอบ แต่ผมแทบไม่ได้จับ Go เลย น่าจะดูผิดที่: https://go.dev/ref/spec#For_statements
แก้ไข: คำตอบอยู่ในตารางรูปแบบ code block นั่นเอง เหมือนผมมองข้ามไปแบบเป็น banner เลย น่าประหลาดใจที่
vเป็น ค่าที่คัดลอกมา ไม่ใช่ referenceรองรับ pointer ไปยัง slot ของ array แต่
for rangeจะคัดลอกแทนที่จะให้ pointer ที่ชี้ไปยังแต่ละ slotvคือintเป็นค่า ไม่ใช่ pointer ไปยัง
intถ้าสนใจก็ไปดูได้: https://github.com/adobe/kratos/blob/93246f92d53feba73743dbf...
https://github.com/StalkR/goircbot/blob/6081ed5d1d74f01767d7...
โดยพื้นฐานแล้ว compiler กำลังเปลี่ยน
go a.Monitor(b)เป็น(&a).Monitor(b)เพราะมีการ dereference อัตโนมัติส่วนที่ว่า “จากผลของงานด้านความเข้ากันได้ไปข้างหน้า Go 1.21 จะไม่พยายามคอมไพล์โค้ดที่ประกาศ
go 1.22ขึ้นไป และใน point release อย่าง Go 1.20.8 กับ Go 1.19.13 ก็ใส่การจัดการพิเศษที่ให้ผลแบบเดียวกันไว้ด้วย ดังนั้นเมื่อ Go 1.22 ออก โค้ดที่เขียนโดยพึ่งพา semantics ใหม่จะไม่มีทางถูกคอมไพล์ด้วย semantics เดิมเลย เว้นแต่จะใช้ Go เวอร์ชันเก่ามากที่ไม่รองรับแล้ว” นี่ทำงานยังไงกันแน่ถ้าแพ็กเกจบางตัวล็อกไว้ที่ 1.22 แล้วผมคอมไพล์ด้วย 1.18 มันจะคอมไพล์ได้ไหม หรือจะ error ว่าต้องใช้คอมไพเลอร์ 1.22?
เพราะใน Go 1.21 มีการเปลี่ยน รูปแบบหมายเลขเวอร์ชัน ในไฟล์
go.modถ้าพยายาม build ด้วย Go 1.18 ก็จะเจอ error อย่างgo.mod:3: invalid go version '1.21.0': must match format 1.23แต่กรณีนี้เป็นแบบนั้นเฉพาะตอนสร้างโมดูลด้วย
go mod initเท่านั้น ถ้าเขียนgo 1.21ในgo.modเอง ก็ build ได้โดยไม่บ่นเป็นฟีเจอร์ที่เจ๋งทีเดียว แต่ก็เป็นพฤติกรรมที่น่าประหลาดใจ และทำให้ลังเลนิดหน่อยเพราะมันจะไปเชื่อมต่อเซิร์ฟเวอร์ที่ Google ควบคุมเพื่อดาวน์โหลดไบนารี
มันเป็นหนึ่งในฟีเจอร์ของ Go ที่ให้ความรู้สึกสองทางมากที่สุด ควบคู่กับ module proxy และผมน่าจะสบายใจกว่านี้มากถ้า Go ถูกดูแลโดยมูลนิธิที่ Google แค่มีส่วนถือหุ้นเท่านั้น
แก้ไข: พอมาคิดดู นี่เป็นกรณีที่โมดูลปัจจุบันประกาศเวอร์ชัน ไม่ใช่ตอน dependency ประกาศเวอร์ชันอื่น จึงไม่ตรงกับคำถามเดิม
ดังนั้นการใช้ Go 1.18 จึงกลายเป็นเรื่องอันตรายอย่างจริงจัง
ใน Go 1.19 น่าจะเกิด compiler error
ยังไง Go ก็ไม่ได้ backport การแก้ security bug ให้ release เก่า ๆ กับ standard library อยู่แล้ว ผมเลยมองว่าการใช้เวอร์ชันพวกนั้นเองก็อันตราย
แต่ถึงจะคอมไพล์ด้วย Go 1.22 โค้ดของคุณก็ยังมี semantics แบบ Go 1.18 อยู่ดี
Go เป็นภาษาที่แปลกมากในบางแง่
เป็นภาษาที่มี ความคิดเห็นแรงมาก แต่ในขณะเดียวกันก็ดูเหมือนเป็นภาษาที่แทบไม่มีความคิดเห็นเลย
ผมไม่แน่ใจว่าความต่างระหว่างโค้ดที่วนผ่าน
c.informerMapกับโค้ดที่วนผ่านalarmsคืออะไร แต่ถ้าให้เดา ตัวแปรลูปฝั่งหนึ่งอาจเป็น pointer ส่วนอีกฝั่งเป็น valueเพราะการเรียกเมธอดใช้ pointer receiver กรณีที่เป็น value คอมไพเลอร์อาจใส่ reference ไปยัง receiver ให้โดยอัตโนมัติหรือเปล่า?
https://github.com/adobe/kratos/blob/93246f92d53feba73743dbf...
https://github.com/StalkR/goircbot/blob/6081ed5d1d74f01767d7...
ความต่างคือในฝั่งหนึ่ง
informerเป็น interface การเรียกเมธอดจึงถูกตีความเป็นinformer.Runทันทีและไม่มีปัญหาส่วนอีกฝั่ง
aเป็น structAlarmและถูกคัดลอกเป็น value ขณะที่เมธอดMonitorรับ pointer receiverดังนั้นคอมไพเลอร์จึงแปลง
go a.Monitor(b)ให้เป็นgo (&a).Monitor(b)โดยพฤตินัย และนั่นสร้าง reference ไปยังตัวแปรลูปจนทำให้เกิดปัญหาส่วนอันที่สอง ผมเดาว่า
aสุดท้ายจะมีแค่ค่าของ element สุดท้ายในalarmsจึงเกิดปัญหาเดิมตามที่บทความอธิบายความรู้วงในของผมมีแค่นั้น แต่ slice มี backing array บน heap จึงมี pointer หรือ reference เข้าไปเกี่ยวข้องอยู่บ้าง
อ่านแล้วรู้สึกโล่งใจมาก
นี่คือการแก้ ข้อบกพร่อง ใหญ่ที่สุดอย่างหนึ่งของ Go
ถ้าเขียน
foo, err := getFoo(); if err != nil ...แล้วต่อด้วยbar, err := getBar(); fmt.Println(bar)ก็จะพลาดการตรวจ error ของgetBarด้วยกฎ scope ทำให้ pattern
if foo, err := getFoo(); err != nilจัดการยากมากเมื่อ nesting ลึกขึ้นแค่นิดเดียวนอกจากนี้ยังนำสถานะที่ไม่ถูกต้องเข้ามาอีกด้วย ถ้า
getFooคืน error แล้วควรคืนอะไร? ทำให้ต้องคิดว่าจะเปลี่ยน API ให้คืน pointer เพื่อคืนnilหรือจะวางอ็อบเจกต์ที่สร้างมาบางส่วนซึ่งอยู่ในสถานะไม่ valid ไว้ดี