
range loop เป็นสิ่งที่ชาว golang ต้องใช้กันอย่างสม่ำเสมอ และหนึ่งในนั้นคือการนำ address ของ variable ใน range loop มาใช้ และ!! พบว่า address จะยังคงเป็นตัวเดิมเสมอ นั้นเพราะ ในการเรียกใช้ range loop ทุกครั้ง จะเป็นการ reassign value ให้กับ value ของ range 😂 อ่านแล้วอาจจะงง ว่าแล้วก็ไปดู code กันเพื่อให้เห็นภาพมากขึ้น และดูวิธีแก้แบบง่ายๆกัน
เริ่มจาก code ง่ายๆชุดนี้กัน
เมื่อเราทำการ run ก็จะได้ผลลัพธ์แบบนี้
----- buggyLoop ------
Soldier with name: <Too> and pointer: <0xc00000c060>
Soldier with name: <Pawit> and pointer: <0xc00000c060>
Soldier with name: <Tone> and pointer: <0xc00000c060>
soldier <Tone> with corruption: <false> and pointer: <0xc00000c060>
soldier <Tone> with corruption: <false> and pointer: <0xc00000c060>
soldier <Tone> with corruption: <false> and pointer: <0xc00000c060>
จะเห็นว่า Too Pawit Tone จาก range loop แรกใน func buggyLoop() จะมี address เดียวกัน และเมื่อส่ง address ไปให้ range loop สอง ก็ได้ value เป็น Tone และ address เดียวกันทั้งหมด ทำไมจึงเป็นแบบนั้น???
คำตอบคือ ใน range loop ทุกครั้ง จะทำการ assigns iteration values (value ในรอบการวนลูปนั้นๆ) ให้กับ iteration variables ตัวเดิม หรือจากตัวอย่าง
for _, soldier := range soldiers {soldier := soldier // declare local temporary variables. -> new temporary addr in memoryfmt.Printf("Soldier with name: <%s> and pointer: <%p>\n", soldier.name, &soldier)soldierPtrs = append(soldierPtrs, &soldier)}
เมื่อเรา loop รอบแรกจะมีการสร้าง soldier ขึ้นมาซึ่งประกอบไปด้วย value และ address แต่เมื่อ จบ loop แรกและทำการ loop ครั้งถัดไป จะไม่มีการสร้าง variable ใหม่ในการ loop แต่จะ reassign iteration valueให้ soldier(variable ตัวเดิม)นั้นเอง ดังนั้นจะเห็นว่า ทุกครั้งที่ print ออกมา address จะเป็นตัวเดิม แต่ value ไม่เหมือนเดิม
A “for” statement with a “range” clause iterates through all entries of an array, slice, string or map, or values received on a channel. For each entry it assigns iteration values to corresponding iteration variables if present and then executes the block.
— The Go Programming Language Specification
โอเค ทีนี้เราคงเข้าใจธรรมชาติของ range loop มากขึ้น เอาหละ งั้นมาหาทางนำ address มาใช้กัน ซึ่งสิ่งที่ทำก็ไม่มีไรมากเลย แค่เราจะ สร้าง variable ใหม่ มารับช่วงต่อในการ loop ครั้งนั้นๆขึ้นมา ดังตัวอย่าง code ข้างล่าง
เมื่อลอง run จะได้ผลลัพธ์ใหม่คือ
----- fixedLoop ------
Soldier with name: <Too> and pointer: <0xc00000c0a0>
Soldier with name: <Pawit> and pointer: <0xc00000c0c0>
Soldier with name: <Tone> and pointer: <0xc00000c0e0>
soldier <Too> with corruption: <true> and pointer: <0xc00000c0a0>
soldier <Pawit> with corruption: <true> and pointer: <0xc00000c0c0>
soldier <Tone> with corruption: <false> and pointer: <0xc00000c0e0>
จากการที่ for, if and switch เมื่อมีการเรียกใช้จะนับว่าเป็น implicit block ซึ่งเราสามารถ redeclare variable ได้
— The Go Programming Language Specification
ดังนั้นเราจึง redeclare soldier := soldier หรือก็คือ ประกาศ temporary variable ใหม่ขึ้นมา และให้ value และ type เป็นเหมือน soldier เดิม ดังนั้นเมื่อเป็น variable ใหม่ ก็ได้ address ใหม่ทุกครั้งที่ loop และเมื่อส่งต่อ address ไปให้ range ถัดไปก็ได้ผลลัพธ์ตามด้านบนเลย!!
สรุป
ทุกการ range loop จะเป็นการ re-used variable ตัวเดิมเพียงแค่ reassign value ในแต่ละรอบการ loop ซึ่งการเราอยากจะใช้ address ก็เพียงแค่ redeclare variable ใหม่ขึ้นมาแค่นั้นก็สามารถสร้าง variable เพิ่มเก็บ value และ address ใหม่ได้
เพื่อใครอยากอ่านแบบจัดเต็ม ก็มีบทความที่เขียนเรื่องนี้ไว้ และผมก็นำมาดัดแปลง ตามอ่านได้ที่นี่เลย
ปล. ถ้าใครชอบก็อย่าลืมกดปรบมือ กด follow เป็นกำลังใจให้ด้วยนะครับ