Evolving the Go Standard Library with math/rand/v2

Bài tóm tắt Go 2024 4. Điều chỉnh Độ khó: Trung bình

Go · 2024 · 4. Điều chỉnh · Độ khó: Trung bình — bài gốc

Dự án ngôn ngữ mã nguồn mở, không phải hệ của một công ty. Wiki giữ phần cách đổi một API dùng rộng khi bị ràng buộc bởi cam kết tương thích.

Bối cảnh

  • Từ Go 1 (3/2012), thư viện chuẩn bị ràng buộc bởi cam kết tương thích. Với math/rand, cam kết còn gồm chuỗi số sinh ra từ một seed: Go 1.2 thử đổi và làm hỏng test của người dùng, nên từ đó output theo seed được coi là API (có test giữ).
  • Các lỗi tích luỹ: bộ sinh Go 1 trạng thái gần 5 KB (PCG chỉ 16 byte, số tốt hơn); interface Source trả 63 bit và seed int64 cứng; không rõ ai seed bộ sinh toàn cục, nên thư viện tự seed trong init và một package chỉ chạy đúng nhờ tình cờ package khác cũng được import (đã gặp trong codebase lớn); bộ sinh toàn cục có mutex, khi chạy song song khoá đắt hơn việc sinh số; thuật toán Lemire nhanh hơn 20–30% nhưng đổi output; math/rand.Read dễ bị dùng nhầm cho khoá mật mã.

Thay đổi chính

  • Sửa v1 trước, đến đâu hay đến đó: Go 1.8 thêm interface mở rộng Source64 (tương thích, hơi vụng); Go 1.20 tự seed bộ sinh toàn cục và deprecate rand.Seed, có GODEBUG để quay lại hành vi cũ. Lập luận: thêm bớt một lời gọi rand ở bất kỳ package nào vốn đã làm đổi chuỗi toàn cục, nên tự seed không tệ hơn. Khi không còn phải lặp lại chuỗi toàn cục, thay bằng bộ sinh theo thread (wyrand, vốn có trong runtime), bỏ mutex; chương trình còn gọi Seed thì rơi về bộ sinh cũ có khoá. Deprecate Read khiến editor gắn cờ, và người dùng phát hiện mình đang dùng nhầm trong ngữ cảnh mật mã.
  • Phần còn lại vào math/rand/v2 (Go 1.22) sau thảo luận GitHub và proposal: bỏ bộ sinh Go 1, thêm PCG và ChaCha8, sửa interface, bỏ Read, dùng thuật toán nhanh.
  • Nguyên tắc cho mọi gói v2 sau này: đường import .../v2 để v1 và v2 cùng tồn tại trong một chương trình, cho phép chuyển dần; không gây xáo trộn không cần thiết, lấy gói cũ làm điểm xuất phát; v2 không bỏ rơi người dùng v1, lý tưởng là v1 thành lớp bọc mỏng quanh v2 để vẫn nhận bản sửa lỗi và tối ưu (math/rand chỉ làm được một phần vì ràng buộc lặp lại; encoding/json/v2 thiết kế để làm được). Mỗi lần một gói, nhiều gói không cần v2.

Vì sao

  • Bản v2 chỉ có ích cho ai chuyển sang, còn sửa v1 có ích cho mọi người; nên v2 là lựa chọn sau cùng, chỉ cho phần không thể sửa tương thích.

Bài học cho SWE

  • Hành vi quan sát được (ở đây là chuỗi số theo seed) trở thành hợp đồng dù không ai định thế; biết điều đó trước khi tối ưu.
  • Trạng thái toàn cục không có chủ (ai seed?) sinh ra phụ thuộc ngầm giữa các module; mặc định an toàn ở nền tảng xoá được cả một lớp lỗi.
  • Đổi API cho nhiều người dùng: sửa tại chỗ đến mức tương thích cho phép, deprecate có công cụ hỗ trợ, có cờ quay lại, rồi mới ra phiên bản mới chạy song song với bản cũ.

Liên quan