Blog Go

Đề xuất Quản lý Phiên bản Gói trong Go

Russ Cox
26 March 2018

Giới thiệu

Tám năm trước, nhóm Go đã giới thiệu goinstall (dẫn đến go get) và cùng với đó là các đường dẫn import phi tập trung, giống URL mà các nhà phát triển Go quen thuộc ngày nay. Sau khi chúng tôi phát hành goinstall, một trong những câu hỏi đầu tiên mọi người hỏi là làm thế nào để tích hợp thông tin phiên bản. Chúng tôi thừa nhận rằng mình không biết. Trong một thời gian dài, chúng tôi tin rằng vấn đề quản lý phiên bản gói sẽ được giải quyết tốt nhất bởi một công cụ bổ sung, và chúng tôi khuyến khích mọi người tạo ra một công cụ như vậy. Cộng đồng Go đã tạo ra nhiều công cụ với các cách tiếp cận khác nhau. Mỗi công cụ giúp chúng tôi hiểu rõ hơn về vấn đề, nhưng đến giữa năm 2016, rõ ràng là đã có quá nhiều giải pháp. Chúng tôi cần áp dụng một công cụ chính thức duy nhất.

Sau một cuộc thảo luận cộng đồng bắt đầu tại GopherCon vào tháng 7 năm 2016 và tiếp tục vào mùa thu, tất cả chúng tôi tin rằng câu trả lời sẽ là tuân theo cách tiếp cận quản lý phiên bản gói được Cargo của Rust minh họa, với các phiên bản ngữ nghĩa được gắn thẻ, một manifest, một lock file, và bộ giải SAT để quyết định phiên bản nào cần dùng. Sam Boyer đã dẫn đầu một nhóm để tạo ra Dep, theo kế hoạch tổng quan này, và chúng tôi dự định dùng nó làm mô hình cho việc tích hợp lệnh go. Nhưng khi tôi tìm hiểu thêm về ý nghĩa của cách tiếp cận Cargo/Dep, tôi dần nhận ra rằng Go sẽ được hưởng lợi nếu thay đổi một số chi tiết, đặc biệt liên quan đến khả năng tương thích ngược.

Tác động của Tương thích

Tính năng mới quan trọng nhất của Go 1 không phải là một tính năng ngôn ngữ. Đó là sự nhấn mạnh của Go 1 vào tương thích ngược. Trước đó, chúng tôi đã phát hành các ảnh chụp bản phát hành ổn định khoảng hàng tháng, mỗi lần với các thay đổi không tương thích đáng kể. Chúng tôi quan sát thấy sự gia tăng đáng kể về sự quan tâm và áp dụng ngay sau khi phát hành Go 1. Chúng tôi tin rằng cam kết tương thích khiến các nhà phát triển cảm thấy thoải mái hơn nhiều khi phụ thuộc vào Go cho việc sử dụng trong môi trường production và là lý do chính khiến Go phổ biến ngày nay. Từ năm 2013, FAQ Go đã khuyến khích các nhà phát triển gói cung cấp cho người dùng của họ những kỳ vọng tương thích tương tự. Chúng tôi gọi đây là quy tắc tương thích import: “Nếu một gói cũ và một gói mới có cùng đường dẫn import, gói mới phải tương thích ngược với gói cũ.”

Độc lập với điều đó, đặt phiên bản ngữ nghĩa đã trở thành tiêu chuẩn de facto để mô tả phiên bản phần mềm trong nhiều cộng đồng ngôn ngữ, bao gồm cộng đồng Go. Sử dụng phiên bản ngữ nghĩa, các phiên bản sau được mong đợi tương thích ngược với các phiên bản trước, nhưng chỉ trong một major version duy nhất: v1.2.3 phải tương thích với v1.2.1 và v1.1.5, nhưng v2.3.4 không cần tương thích với bất kỳ phiên bản nào trong số đó.

Nếu chúng ta áp dụng phiên bản ngữ nghĩa cho các gói Go, như hầu hết các nhà phát triển Go mong đợi, thì quy tắc tương thích import đòi hỏi rằng các major version khác nhau phải sử dụng các đường dẫn import khác nhau. Quan sát này dẫn chúng tôi đến đặt tên import theo phiên bản ngữ nghĩa, trong đó các phiên bản bắt đầu từ v2.0.0 bao gồm major version trong đường dẫn import: my/thing/v2/sub/pkg.

Một năm trước, tôi tin mạnh mẽ rằng việc bao gồm số phiên bản trong đường dẫn import hay không phần lớn là vấn đề sở thích, và tôi hoài nghi rằng việc có chúng có đặc biệt thanh lịch không. Nhưng quyết định hóa ra không phải là vấn đề sở thích mà là vấn đề logic: tương thích import và phiên bản ngữ nghĩa cùng nhau đòi hỏi đặt tên import theo phiên bản ngữ nghĩa. Khi tôi nhận ra điều này, sự tất yếu logic đã khiến tôi ngạc nhiên.

Tôi cũng ngạc nhiên khi nhận ra rằng có một con đường logic thứ hai, độc lập dẫn đến đặt tên import theo phiên bản ngữ nghĩa: sửa chữa code dần dần hay nâng cấp code một phần. Trong một chương trình lớn, việc mong đợi tất cả các gói trong chương trình cập nhật từ v1 lên v2 của một dependency cụ thể cùng một lúc là không thực tế. Thay vào đó, phải có thể một phần của chương trình tiếp tục dùng v1 trong khi các phần khác đã nâng cấp lên v2. Nhưng khi đó, build của chương trình, và binary cuối cùng của chương trình, phải bao gồm cả v1 lẫn v2 của dependency. Đặt cho chúng cùng một đường dẫn import sẽ dẫn đến nhầm lẫn, vi phạm điều chúng tôi có thể gọi là quy tắc import duy nhất: các gói khác nhau phải có các đường dẫn import khác nhau. Cách duy nhất để có nâng cấp code từng phần, tính duy nhất của import, phiên bản ngữ nghĩa là áp dụng đặt tên import theo phiên bản ngữ nghĩa.

Tất nhiên, có thể xây dựng các hệ thống sử dụng phiên bản ngữ nghĩa mà không có đặt tên import theo phiên bản ngữ nghĩa, nhưng chỉ bằng cách từ bỏ nâng cấp code từng phần hoặc tính duy nhất của import. Cargo cho phép nâng cấp code từng phần bằng cách từ bỏ tính duy nhất import: một đường dẫn import nhất định có thể có nghĩa khác nhau ở các phần khác nhau của một build lớn. Dep đảm bảo tính duy nhất import bằng cách từ bỏ nâng cấp code từng phần: tất cả các gói liên quan đến một build lớn phải tìm được một phiên bản thống nhất duy nhất của một dependency nhất định, nêu lên khả năng rằng các chương trình lớn sẽ không thể build được. Cargo đúng khi nhấn mạnh nâng cấp code từng phần, điều quan trọng cho việc phát triển phần mềm quy mô lớn. Dep cũng đúng khi nhấn mạnh tính duy nhất import. Các cách dùng phức tạp của hỗ trợ vendoring hiện tại của Go có thể vi phạm tính duy nhất import. Khi xảy ra, các vấn đề kết quả khá khó khăn cho cả nhà phát triển và công cụ để hiểu. Việc quyết định giữa nâng cấp code từng phần và tính duy nhất import đòi hỏi dự đoán cái nào sẽ đau hơn khi từ bỏ. Đặt tên import theo phiên bản ngữ nghĩa cho phép chúng ta tránh lựa chọn này và giữ cả hai.

Tôi cũng ngạc nhiên khi khám phá ra rằng tương thích import đơn giản hóa việc lựa chọn phiên bản đến mức nào, đây là vấn đề quyết định phiên bản gói nào sử dụng cho một build nhất định. Các ràng buộc của Cargo và Dep làm cho việc lựa chọn phiên bản tương đương với giải bài toán thỏa mãn Boolean, nghĩa là có thể rất tốn kém để xác định liệu một cấu hình phiên bản hợp lệ có tồn tại không. Và sau đó có thể có nhiều cấu hình hợp lệ mà không có tiêu chí rõ ràng để chọn cái “tốt nhất”. Việc dựa vào tương thích import thay vào đó cho phép Go sử dụng một thuật toán tuyến tính đơn giản để tìm cấu hình tốt nhất duy nhất, luôn tồn tại. Thuật toán này, mà tôi gọi là lựa chọn phiên bản tối thiểu, lần lượt loại bỏ nhu cầu các tệp lock và manifest riêng biệt. Nó thay thế chúng bằng một tệp cấu hình duy nhất, ngắn gọn, được chỉnh sửa trực tiếp bởi cả nhà phát triển và công cụ, vẫn hỗ trợ các build có thể tái tạo.

Kinh nghiệm của chúng tôi với Dep chứng minh tác động của tính tương thích. Theo gương Cargo và các hệ thống trước đó, chúng tôi thiết kế Dep để từ bỏ tương thích import như một phần của việc áp dụng phiên bản ngữ nghĩa. Tôi không tin chúng tôi đã quyết định điều này một cách có chủ ý; chúng tôi chỉ theo những hệ thống đó. Kinh nghiệm trực tiếp của việc sử dụng Dep đã giúp chúng tôi hiểu chính xác hơn bao nhiêu phức tạp được tạo ra bởi việc cho phép các đường dẫn import không tương thích. Việc khôi phục quy tắc tương thích import bằng cách giới thiệu đặt tên import theo phiên bản ngữ nghĩa loại bỏ sự phức tạp đó, dẫn đến một hệ thống đơn giản hơn nhiều.

Tiến trình, Prototype và Đề xuất

Dep được phát hành vào tháng 1 năm 2017. Mô hình cơ bản của nó, code được gắn thẻ với phiên bản ngữ nghĩa, cùng với một tệp cấu hình chỉ định các yêu cầu dependency, là một bước tiến rõ ràng so với hầu hết các công cụ vendoring Go, và sự hội tụ về Dep cũng là một bước tiến rõ ràng. Tôi hoàn toàn khuyến khích việc áp dụng nó, đặc biệt để giúp các nhà phát triển quen với việc suy nghĩ về phiên bản gói Go, cho cả code của họ và các dependency của họ. Mặc dù Dep rõ ràng đang đưa chúng ta đi đúng hướng, tôi vẫn còn lo ngại về con quỷ phức tạp trong các chi tiết. Tôi đặc biệt lo ngại về việc Dep thiếu hỗ trợ cho nâng cấp code dần dần trong các chương trình lớn. Trong suốt năm 2017, tôi đã nói chuyện với nhiều người, bao gồm Sam Boyer và phần còn lại của nhóm làm việc quản lý gói, nhưng không ai trong chúng tôi có thể thấy bất kỳ cách rõ ràng nào để giảm sự phức tạp. (Tôi đã tìm thấy nhiều cách tiếp cận thêm vào nó.) Khi gần cuối năm, vẫn có vẻ như các bộ giải SAT và các build không thể thỏa mãn có thể là điều tốt nhất chúng ta có thể làm.

Vào giữa tháng 11, một lần nữa cố gắng xem Dep có thể hỗ trợ nâng cấp code dần dần như thế nào, tôi nhận ra rằng lời khuyên cũ của chúng tôi về tương thích import đã ngụ ý đặt tên import theo phiên bản ngữ nghĩa. Điều đó có vẻ như một bước đột phá thực sự. Tôi đã viết bản nháp đầu tiên của bài blog đặt tên import theo phiên bản ngữ nghĩa, kết luận bằng cách đề xuất Dep áp dụng quy ước đó. Tôi đã gửi bản nháp cho những người tôi đã nói chuyện, và nó gây ra phản ứng rất mạnh: mọi người đều yêu thích hoặc ghét nó. Tôi nhận ra rằng tôi cần làm việc ra nhiều hơn các hàm ý của đặt tên import theo phiên bản ngữ nghĩa trước khi lưu hành ý tưởng thêm, và tôi bắt đầu làm điều đó.

Vào giữa tháng 12, tôi phát hiện ra rằng tương thích import và đặt tên import theo phiên bản ngữ nghĩa cùng nhau cho phép cắt giảm việc lựa chọn phiên bản xuống còn lựa chọn phiên bản tối thiểu. Tôi đã viết một cài đặt cơ bản để chắc chắn rằng tôi hiểu nó, dành một thời gian để học lý thuyết đằng sau lý do nó đơn giản như vậy, và tôi đã viết bản nháp của bài mô tả nó. Dù vậy, tôi vẫn không chắc liệu cách tiếp cận có thực tế không trong một công cụ thực sự như Dep. Rõ ràng là cần có một prototype.

Vào tháng 1, tôi bắt đầu làm việc trên một wrapper lệnh go đơn giản cài đặt đặt tên import theo phiên bản ngữ nghĩa và lựa chọn phiên bản tối thiểu. Các test nhỏ hoạt động tốt. Gần cuối tháng, wrapper đơn giản của tôi có thể build Dep, một chương trình thực sự sử dụng nhiều gói được phiên bản hóa. Wrapper vẫn chưa có giao diện dòng lệnh, nhưng cách tiếp cận rõ ràng là khả thi.

Tôi đã dành ba tuần đầu của tháng 2 để biến wrapper thành lệnh go đầy đủ được phiên bản hóa, vgo; viết các bản nháp của loạt bài blog giới thiệu vgo; và thảo luận với Sam Boyer, nhóm làm việc quản lý gói, và nhóm Go. Rồi tôi dành tuần cuối của tháng 2 để cuối cùng chia sẻ vgo và các ý tưởng đằng sau nó với toàn bộ cộng đồng Go.

Ngoài các ý tưởng cốt lõi về tương thích import, đặt tên import theo phiên bản ngữ nghĩa và lựa chọn phiên bản tối thiểu, prototype vgo giới thiệu một số thay đổi nhỏ hơn nhưng đáng kể được thúc đẩy bởi tám năm kinh nghiệm với goinstallgo get: khái niệm mới về Go module, là tập hợp các gói được phiên bản hóa như một đơn vị; các build có thể xác minh và được xác minh; và nhận thức về phiên bản trong toàn bộ lệnh go, cho phép làm việc ngoài $GOPATH và loại bỏ (hầu hết) các thư mục vendor.

Kết quả của tất cả điều này là đề xuất chính thức của Go, mà tôi đã nộp tuần trước. Mặc dù có thể trông như một cài đặt hoàn chỉnh, nó vẫn chỉ là một prototype, một prototype mà tất cả chúng ta cần làm việc cùng nhau để hoàn thiện. Bạn có thể tải xuống và thử prototype vgo từ golang.org/x/vgo, và bạn có thể đọc Tour of Versioned Go để cảm nhận về việc sử dụng vgo như thế nào.

Con đường phía trước

Đề xuất tôi đã nộp tuần trước chính xác là: một đề xuất ban đầu. Tôi biết có những vấn đề với nó mà nhóm Go và tôi không thể nhìn thấy, vì các nhà phát triển Go sử dụng Go theo nhiều cách thông minh mà chúng tôi không biết. Mục tiêu của quy trình phản hồi đề xuất là để tất cả chúng ta cùng làm việc để xác định và giải quyết các vấn đề trong đề xuất hiện tại, để đảm bảo rằng cài đặt cuối cùng được tích hợp trong một bản phát hành Go trong tương lai hoạt động tốt cho càng nhiều nhà phát triển càng tốt. Hãy chỉ ra các vấn đề tại vấn đề thảo luận đề xuất. Tôi sẽ cập nhật tóm tắt thảo luậnFAQ khi phản hồi đến.

Để đề xuất này thành công, hệ sinh thái Go nói chung, và đặc biệt là các dự án Go lớn ngày nay, sẽ cần áp dụng quy tắc tương thích import và đặt tên import theo phiên bản ngữ nghĩa. Để đảm bảo điều đó có thể xảy ra suôn sẻ, chúng tôi cũng sẽ tổ chức các buổi phản hồi người dùng qua hội nghị video với các dự án có câu hỏi về cách tích hợp đề xuất phiên bản mới vào codebase của họ hoặc có phản hồi về trải nghiệm của họ. Nếu bạn muốn tham gia vào một buổi như vậy, hãy gửi email cho Steve Francia tại spf@golang.org.

Chúng tôi mong chờ (cuối cùng!) cung cấp cho cộng đồng Go một câu trả lời duy nhất, chính thức cho câu hỏi về cách tích hợp quản lý phiên bản gói vào go get. Cảm ơn tất cả những người đã giúp chúng tôi đến đây, và tất cả những người sẽ giúp chúng tôi tiến về phía trước. Chúng tôi hy vọng rằng với sự giúp đỡ của bạn, chúng tôi có thể tạo ra thứ gì đó mà các nhà phát triển Go sẽ yêu thích.

Bài tiếp theo: Thương hiệu mới của Go
Bài trước: Kết quả Khảo sát Go 2017
Mục lục blog