การฝัง Struct ใน Go สร้างความขัดแย้งของฟิลด์ที่ซ่อนอยู่แต่ Compile สำเร็จ

ทีมชุมชน BigGo
การฝัง Struct ใน Go สร้างความขัดแย้งของฟิลด์ที่ซ่อนอยู่แต่ Compile สำเร็จ

ฟีเจอร์การฝัง 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