ฟีเจอร์การฝัง struct ของ Go ช่วยให้นักพัฒนาสามารถสร้างชนิดข้อมูลโดยการรวม struct หนึ่งเข้าไปในอีก struct หนึ่ง ทำให้เกิดทางลัดในการเข้าถึงฟิลด์ที่ซ้อนกัน แม้ว่าจะดูสะดวก แต่อาจนำไปสู่พฤติกรรมที่ไม่คาดคิดเมื่อ struct ที่ฝังไว้หลายตัวมีฟิลด์ที่ชื่อเดียวกัน
อันตรายที่ซ่อนอยู่ของความขัดแย้งในชื่อฟิลด์
เมื่อ Go พบฟิลด์หลายตัวที่มีชื่อเหมือนกันใน struct ที่ฝังไว้ มันจะไม่แสดงข้อผิดพลาดในการ compile ตามที่นักพัฒนาหลายคนคาดหวัง แต่จะทำตามกฎ ความลึกที่ตื้นที่สุดชนะ โดยเลือกฟิลด์ที่อยู่ใกล้ระดับบนสุดที่สุดโดยอัตโนมัติ นั่นหมายความว่าฟิลด์ที่ความลึก 1 จะเอาชนะฟิลด์ที่ความลึก 2 เสมอ แม้ว่าฟิลด์ที่ลึกกว่าจะถูกกำหนดล่าสุดหรือดูเหมือนจะเกี่ยวข้องกับบริบทมากกว่า
พฤติกรรมนี้กลายเป็นปัญหาโดยเฉพาะในแอปพลิเคชันในโลกจริงที่นักพัฒนาอาจสร้างความขัดแย้งในการตั้งชื่อโดยไม่รู้ตัว โค้ดจะ compile สำเร็จ การทดสอบอาจผ่าน แต่โปรแกรมจะใช้แหล่งข้อมูลที่ผิดอย่างเงียบ ๆ บั๊กประเภทนี้อาจติดตามได้ยากมากเพราะไม่มีสัญญาณที่ชัดเจนว่ามีอะไรผิดปกติ
กฎการแก้ไขฟิลด์การฝัง Struct ใน Go:
- ฟิลด์ที่อยู่ในระดับความลึกที่ตื้นกว่าจะมีความสำคัญเหนือฟิลด์ที่อยู่ในระดับที่ลึกกว่าเสมอ
- การขัดแย้งของฟิลด์ในระดับเดียวกันจะทำให้เกิดข้อผิดพลาดในการคอมไพล์
- กฎนี้ใช้กับทั้งฟิลด์ struct และ method
- การเข้าถึงแบบชัดเจนผ่าน path เต็ม (เช่น
opts.BarService.URL
) ยังคงใช้งานได้เมื่อเกิดการขัดแย้ง
ชุมชนต่อต้านการ Embedding
ชุมชน Go มีความสงสัยเพิ่มขึ้นเกี่ยวกับการฝัง struct ตลอดหลายปีที่ผ่านมา นักพัฒนาที่มีประสบการณ์หลายคนหลีกเลี่ยงฟีเจอร์นี้ทั้งหมด โดยบางคนเปรียบเทียบกับการดำเนินการ unsafe ที่ควรต้องมีการ import พิเศษเพื่อใช้งาน ฉันทามติดูเหมือนจะเป็นว่าแม้การ embedding อาจประหยัดโค้ดได้ไม่กี่บรรทัดในตอนแรก แต่มักจะนำไปสู่ปัญหาการบำรุงรักษาและบั๊กที่ละเอียดอ่อนซึ่งมีน้ำหนักมากกว่าประโยชน์ด้านความสะดวก
ตลอดระยะเวลา ~10 ปีในการเขียน Go อัตราส่วนของการฝัง struct ต่อการเสียใจที่ฝัง struct ของฉันเกือบจะ 1:1 ฉันไม่ฝัง struct อีกแล้ว มันเกือบจะเป็นความผิดพลาดเสมอ
เมื่อการ Embedding อาจยังมีประโยชน์
แม้จะมีการวิพากษ์วิจารณ์ นักพัฒนาบางคนโต้แย้งว่าการ embedding มีกรณีการใช้งานที่ถูกต้อง สถานการณ์ที่ยอมรับกันมากที่สุดเกี่ยวข้องกับการสร้างชนิดข้อมูลแบบ wrapper ที่ต้องการ override เมธอดเฉพาะขณะที่รักษา interface ที่เหลือไว้ รูปแบบนี้ทำงานได้ดีสำหรับ mock object ในการทดสอบหรือเมื่อเพิ่มฟังก์ชันการทำงานให้กับชนิดข้อมูลที่มีอยู่โดยไม่ทำลาย contract ของมัน
กรณีการใช้งานที่ยอมรับอีกกรณีหนึ่งเกี่ยวข้องกับการจัดองค์ประกอบข้อมูลอย่างง่ายสำหรับ discriminated union ที่ struct พื้นฐานมีฟิลด์ทั่วไปและ struct เฉพาะทางฝังมันเพื่อเพิ่มแอตทริบิวต์เฉพาะชนิด อย่างไรก็ตาม แม้แต่ผู้สนับสนุนก็แนะนำให้เปลี่ยนไปใช้การเข้าถึงฟิลด์แบบชัดเจนทันทีที่โครงสร้างกลายเป็นที่ซับซ้อน
กรณีการใช้งาน Embedding ที่แนะนำ:
- Mock objects ที่ override เมธอดเฉพาะของ interface
- Discriminated unions แบบง่ายที่มีฟิลด์พื้นฐานร่วมกัน
- Utility wrappers ที่เพิ่มเมธอดช่วยเหลือให้กับประเภทข้อมูลที่มีอยู่
- Interface composition (มีปัญหาน้อยกว่า struct embedding)
การอภิปรายปรัชญาการออกแบบที่ลึกซึ้งยิ่งขึ้น
ความขัดแย้งเรื่องการ embedding นี้สะท้อนความตึงเครียดที่กว้างขึ้นในปรัชญาการออกแบบของ Go ภาษานี้ส่งเสริมความเรียบง่ายและความชัดเจน แต่กลับรวมฟีเจอร์เช่นการฝัง struct ที่สามารถสร้างพฤติกรรมแบบนัยและยากต่อการดีบั๊ก นักวิจารณ์โต้แย้งว่านี่แสดงถึงความไม่สอดคล้องในหลักการของภาษา ที่ฟีเจอร์บางอย่างให้ความสำคัญกับความสะดวกมากกว่าความชัดเจน
รากฐานของฟีเจอร์นี้ย้อนกลับไปถึง Plan 9 C ที่มีแนวคิด anonymous field ที่คล้ายกันอยู่ แม้ว่าจะให้บริบททางประวัติศาสตร์ แต่นักพัฒนาหลายคนตั้งคำถามว่ารูปแบบที่คล้าย inheritance เช่นนี้เข้ากันได้ดีกับเป้าหมายที่ระบุไว้ของ Go ในเรื่องความเรียบง่ายและความสามารถในการบำรุงรักษาหรือไม่
นักพัฒนา Go ที่มีประสบการณ์ส่วนใหญ่ในขณะนี้แนะนำให้หลีกเลี่ยงการฝัง struct ยกเว้นในสถานการณ์เฉพาะที่เข้าใจดี เมื่อเกิดความขัดแย้งของฟิลด์ สามารถแก้ไขได้โดยการเข้าถึงฟิลด์อย่างชัดเจนผ่าน path เต็ม แต่การป้องกันความขัดแย้งดังกล่าวตั้งแต่แรกยังคงเป็นแนวทางที่ปลอดภัยกว่า
อ้างอิง: Be Careful with Go Struct Embedding