Tính năng Struct Embedding của Go tạo ra xung đột trường ẩn nhưng vẫn biên dịch thành công

Nhóm Cộng đồng BigGo
Tính năng Struct Embedding của Go tạo ra xung đột trường ẩn nhưng vẫn biên dịch thành công

Tính năng struct embedding của Go cho phép các nhà phát triển tạo ra các kiểu dữ liệu phức hợp bằng cách nhúng một struct vào bên trong struct khác, tạo ra các đường tắt để truy cập các trường lồng nhau. Mặc dù điều này có vẻ tiện lợi, nhưng nó có thể dẫn đến hành vi không mong muốn khi nhiều struct được nhúng chứa các trường có cùng tên.

Nguy hiểm ẩn giấu của xung đột tên trường

Khi Go gặp phải nhiều trường có tên giống hệt nhau trong các struct được nhúng, nó không ném ra lỗi biên dịch như nhiều nhà phát triển mong đợi. Thay vào đó, nó tuân theo quy tắc độ sâu nông nhất thắng, tự động chọn trường gần nhất với cấp độ trên cùng. Điều này có nghĩa là một trường ở độ sâu 1 sẽ luôn ghi đè lên trường ở độ sâu 2, ngay cả khi trường sâu hơn được gán gần đây hơn hoặc có vẻ phù hợp hơn với ngữ cảnh.

Hành vi này trở nên đặc biệt có vấn đề trong các ứng dụng thực tế khi các nhà phát triển có thể vô tình tạo ra xung đột tên. Mã nguồn biên dịch thành công, các bài kiểm tra có thể vượt qua, nhưng chương trình lại âm thầm sử dụng nguồn dữ liệu sai. Những lỗi như vậy có thể cực kỳ khó theo dõi vì không có dấu hiệu rõ ràng nào cho thấy có gì đó sai.

Quy tắc phân giải trường nhúng trong Go Struct:

  • Các trường ở độ sâu nông hơn luôn ghi đè các trường ở mức sâu hơn
  • Xung đột trường cùng cấp gây ra lỗi biên dịch
  • Quy tắc này áp dụng cho cả trường struct và phương thức
  • Truy cập rõ ràng qua đường dẫn đầy đủ (ví dụ: opts.BarService.URL) vẫn hoạt động khi có xung đột

Phản ứng dữ dội của cộng đồng đối với Embedding

Cộng đồng Go đã ngày càng hoài nghi về struct embedding qua các năm. Nhiều nhà phát triển có kinh nghiệm hiện tại hoàn toàn tránh tính năng này, với một số người so sánh nó với các thao tác không an toàn mà cần phải có import đặc biệt để sử dụng. Sự đồng thuận có vẻ như là mặc dù embedding có thể tiết kiệm vài dòng mã ban đầu, nhưng nó thường dẫn đến những rắc rối bảo trì và lỗi tinh vi vượt quá bất kỳ lợi ích tiện lợi nào.

Trong suốt khoảng 10 năm viết Go , tỷ lệ embedding một struct so với việc hối tiếc khi embedding một struct của tôi gần như là 1:1. Tôi không còn embed struct nữa. Đó hầu như luôn là một sai lầm.

Khi nào Embedding vẫn có thể hữu ích

Bất chấp những lời chỉ trích, một số nhà phát triển cho rằng embedding có những trường hợp sử dụng hợp lệ. Kịch bản được chấp nhận phổ biến nhất liên quan đến việc tạo ra các kiểu wrapper cần ghi đè các phương thức cụ thể trong khi vẫn bảo tồn phần còn lại của interface. Mẫu này hoạt động tốt cho các đối tượng mock trong kiểm thử hoặc khi thêm chức năng vào các kiểu hiện có mà không phá vỡ hợp đồng của chúng.

Một trường hợp sử dụng được chấp nhận khác liên quan đến việc tổ hợp dữ liệu đơn giản cho discriminated union, trong đó một struct cơ sở chứa các trường chung và các struct chuyên biệt nhúng nó để thêm các thuộc tính cụ thể theo kiểu. Tuy nhiên, ngay cả những người ủng hộ cũng khuyến nghị chuyển sang truy cập trường rõ ràng ngay khi cấu trúc trở nên phức tạp.

Các trường hợp sử dụng Embedding được khuyến nghị:

  • Các đối tượng mock ghi đè các phương thức interface cụ thể
  • Các union phân biệt đơn giản với các trường cơ sở chung
  • Các wrapper tiện ích thêm các phương thức hỗ trợ vào các kiểu hiện có
  • Kết hợp interface (ít có vấn đề hơn so với embedding struct)

Cuộc tranh luận triết lý thiết kế sâu sắc hơn

Cuộc tranh cãi về embedding này phản ánh những căng thẳng rộng lớn hơn trong triết lý thiết kế của Go . Ngôn ngữ này thúc đẩy tính đơn giản và rõ ràng, nhưng lại bao gồm các tính năng như struct embedding có thể tạo ra hành vi ngầm định, khó gỡ lỗi. Các nhà phê bình cho rằng điều này thể hiện sự không nhất quán trong các nguyên tắc của ngôn ngữ, nơi một số tính năng ưu tiên sự tiện lợi hơn là sự rõ ràng.

Nguồn gốc của tính năng này bắt nguồn từ Plan 9 C , nơi các khái niệm trường ẩn danh tương tự đã tồn tại. Mặc dù điều này cung cấp bối cảnh lịch sử, nhiều nhà phát triển đặt câu hỏi liệu những mẫu giống như kế thừa như vậy có phù hợp với các mục tiêu đã nêu của Go về tính đơn giản và khả năng bảo trì hay không.

Hầu hết các nhà phát triển Go có kinh nghiệm hiện tại khuyến nghị tránh struct embedding trừ khi trong những kịch bản rất cụ thể, được hiểu rõ. Khi xung đột trường xảy ra, chúng có thể được giải quyết bằng cách truy cập các trường một cách rõ ràng thông qua đường dẫn đầy đủ của chúng, nhưng việc ngăn chặn những xung đột như vậy ngay từ đầu vẫn là cách tiếp cận an toàn hơn.

Tham khảo: Be Careful with Go Struct Embedding