Ngôn ngữ Go: Những điều chúng ta đã làm đúng và làm sai
(commandcenter.blogspot.com)- Trong 14 năm kể từ khi được công bố dưới dạng mã nguồn mở vào ngày 10/11/2009, Go đã phát triển với mục tiêu không chỉ là một ngôn ngữ mới, mà là cách giúp việc xây dựng phần mềm production trở nên dễ dàng hơn
- Đặc tả chính thức, nhiều triển khai compiler, cross compile dễ dàng, cam kết tương thích sau Go 1.0, thư viện chuẩn và gofmt đã hỗ trợ việc được chấp nhận
- Concurrency để cấu trúc phần mềm server là một điểm mạnh, nhưng ban đầu việc không giải thích đủ sự khác biệt với parallelism và các trường hợp sử dụng phù hợp đã gây nhầm lẫn
- Interface trở thành nền tảng cho thư viện, testing, quản lý dependency và thiết kế thiên về composition; việc thiếu generics sau đó khiến thiết kế đa hình trở nên khó khăn trong hơn 10 năm
- Dù có nhiều thử-sai trong quản lý package, tài liệu và cộng tác cộng đồng, Go hướng tới một môi trường nơi code trông tương tự và tiếp tục build được dù tác giả và thời điểm viết khác nhau
Mục tiêu của Go: cách phát triển phần mềm hơn là ngôn ngữ
- Ngày 10/11/2023 là kỷ niệm 14 năm Go được công bố dưới dạng dự án mã nguồn mở
- Khi công bố vào năm 2009, Ken Thompson, Robert Griesemer, Russ Cox, Ian Taylor, Adam Langley, Jini Kim và những người khác đã chứng kiến việc ra mắt dự án
- Mục tiêu ban đầu của Go không phải là bản thân một ngôn ngữ lập trình mới, mà là viết phần mềm chất lượng cao theo cách tốt hơn
- Nút thắt lúc đó không nằm ở bản thân các tính năng ngôn ngữ, mà ở độ phức tạp của quá trình xây dựng phần mềm server hiện đại tại Google
- Kiểm soát dependency
- Đội ngũ lớn và cơ cấu nhân sự thay đổi
- Khả năng bảo trì
- Testing hiệu quả
- Tận dụng CPU đa nhân và networking
- Đến nay Go vẫn được định nghĩa là một dự án giúp việc xây dựng phần mềm production dễ dàng và năng suất hơn
Linh vật Gopher tạo nên bản sắc cộng đồng
- Go gopher là một trong những yếu tố ban đầu góp phần vào thành công của Go
- Gopher do Renee French tạo ra đóng vai trò là biểu tượng chung của các lập trình viên Go và là ngọn cờ của cộng đồng
- Như hội nghị mang tên GopherCon, gopher tạo nên bầu không khí dự án kết hợp giữa sự xuất sắc về kỹ thuật và niềm vui
- Việc phát hành theo giấy phép Creative Commons Attribution có cả ưu và nhược điểm
- Khuyến khích remix và biến thể, nuôi dưỡng tinh thần cộng đồng
- Điều kiện attribution cũng gây tranh luận, hoặc dẫn đến việc gán nhầm credit cho Renee French đối với những tác phẩm không phải do cô tạo ra
- Việc quản lý để linh vật giữ được tinh thần ban đầu vẫn là một thách thức khó khăn
Những lựa chọn kỹ thuật Go đã làm tốt
- Việc viết đặc tả chính thức trước đã cố định hành vi của các triển khai compiler và giúp nhiều triển khai có thể thống nhất cùng một hành vi
- Có nhận định rằng chỉ một compiler thì không thể trở thành đặc tả
- Bản nháp đặc tả đầu tiên được viết ở tầng 18 của một tòa nhà tại Sydney Darling Harbour
- Nhiều triển khai compiler đã giúp tinh chỉnh ngôn ngữ và đặc tả
- Ngày 7/6/2008, Ian Taylor gửi email nói rằng ông đã đọc bản nháp đặc tả Go và tạo một gcc frontend
- Sau đó nhiều triển khai tương thích khác cũng xuất hiện
- Nhờ làm cho cross compile trở nên dễ dàng, developer có thể làm việc trên nền tảng họ muốn và deploy lên nền tảng cần thiết
- Sau khi tinh chỉnh ngôn ngữ trong Go 1.0, lựa chọn đưa vào cam kết tương thích đã có ảnh hưởng lớn đến việc Go được chấp nhận
- Việc duy trì tương thích mạnh có chi phí, nhưng giảm lo ngại rằng bản phát hành Go mới sẽ làm hỏng dự án
- Thư viện chuẩn cung cấp phần lớn chức năng cần thiết để viết code server thế kỷ 21, đồng thời khiến cộng đồng làm việc trên cùng một bộ công cụ
- Ban đầu, do không có nơi khác để cài đặt code Go, thư viện cũng có phần phình to
- Điều này giúp giảm tình trạng nhiều thư viện biến thể mọc lên và giúp thống nhất cộng đồng
Trải nghiệm phát triển do công cụ và gofmt tạo ra
- Go được thiết kế là ngôn ngữ dễ parse, nên việc tạo công cụ trở nên dễ dàng
- Ban đầu người ta cho rằng cần IDE, nhưng khi việc tooling trở nên dễ hơn, theo thời gian các IDE đã hỗ trợ Go và gopls cũng xuất hiện
- Cùng với compiler, các công cụ phụ trợ như automated testing, coverage và code vetting cũng được cung cấp
- Lệnh go đã tích hợp toàn bộ quá trình build, và nhiều dự án có thể xử lý phần lớn những gì cần cho việc build và bảo trì code Go bằng lệnh này
- gofmt trở thành công cụ có ảnh hưởng không chỉ với Go mà với cả cộng đồng lập trình nói chung
- Robert Griesemer đã chủ trương tạo gofmt ngay từ đầu
- Trước gofmt, chất lượng formatter tự động không cao nên gần như không được dùng
- Thời gian dành cho tranh luận về khoảng trắng và xuống dòng giảm đi
- Khi phần bên trong gofmt trở thành thư viện, có thể parse và sửa AST rồi tạo lại output có thể đọc được
- Các công cụ khác như simplifier, analyzer và công cụ code coverage cũng trở nên khả thi
Concurrency: mạnh mẽ nhưng được giải thích muộn
- Vào năm 2002, phần mềm của Google hầu như tránh thread, và có bầu không khí ngăn cản sử dụng khi trích dẫn quan điểm “threads were bad” của John Ousterhout
- Go bắt đầu từ kinh nghiệm rằng có thể dùng concurrency theo cách tốt hơn so với các package cấp thấp như pthread
- Go đã đóng vai trò quan trọng trong việc giúp thế giới lập trình chấp nhận concurrency như một công cụ mạnh mẽ
- Đặc biệt hiệu quả trong môi trường đa nhân và network
- Ngày nay hầu hết các ngôn ngữ chủ đạo đều có hỗ trợ concurrency
- Go không dùng những tên như coroutine, task hay thread, mà tạo ra thuật ngữ mới goroutine
- async/await là cải tiến lớn so với pthread, nhưng so với goroutine, channel và select, tuy dễ và nhỏ hơn đối với người triển khai, nó có thể đẩy một phần độ phức tạp trở lại cho lập trình viên
- Vấn đề “What Color is Your Function?” của Bob Nystrom được dùng làm ví dụ
- Mô hình CSP của Go phù hợp với ngôn ngữ thủ tục, nhưng để triển khai đúng cần độ phức tạp ở runtime
- Nếu có nhiều triển khai concurrency trong một môi trường thì có thể gây vấn đề, nên Go đưa nó vào ngôn ngữ thay vì thư viện
Hai điều đã làm sai với concurrency
- Nhóm Go ban đầu đã không hướng dẫn đủ về việc nên dùng concurrency ở đâu
- Trường hợp sử dụng chính là làm cho phần mềm server đơn giản hơn trong các thư viện cốt lõi như
net/http - Đây không phải là tính năng cần dùng ở mọi nơi trong mọi chương trình
- Trường hợp sử dụng chính là làm cho phần mềm server đơn giản hơn trong các thư viện cốt lõi như
- Việc giải thích muộn sự khác biệt giữa parallelism và concurrency cũng là một vấn đề
- Nhiều developer cố dùng goroutine để parallelize code nhằm làm nó nhanh hơn, nhưng lại bối rối khi kết quả chậm hơn
- Code concurrent chỉ có thể nhanh hơn nhờ parallelization khi bản thân bài toán có tính parallel, chẳng hạn như xử lý HTTP request
- Để bù đắp, bài nói “Concurrency is not Parallelism” đã được trình bày tại Waza năm 2012
- Lời giải thích này lẽ ra phải được đưa ra sớm hơn
- Dù vậy, Go đã góp phần phổ biến concurrency như một cách cấu trúc phần mềm server
Interface và thiết kế thiên về composition
- Interface là ý tưởng cốt lõi phân biệt Go, cùng với concurrency
- Interface của Go gần như là câu trả lời của Go cho thiết kế hướng đối tượng dựa trên hành vi theo nghĩa ban đầu
- Cách tiếp cận động, không cần khai báo trước type nào triển khai interface nào, ban đầu bị chỉ trích nhưng rất quan trọng với lập trình theo phong cách Go
- Thư viện chuẩn, testing và quản lý dependency phụ thuộc nhiều vào tính chất mở của interface
sortcủa Go được triển khai dưới dạng các hàm hoạt động trên interface- Khác với phong cách hướng đối tượng mà nhiều người quen thuộc, nhưng đó là một ý tưởng mạnh mẽ
- Các interface nhỏ như
Reader,Writervà empty interface đã trở thành nền tảng của thư viện Go- Trung bình ba interface nổi tiếng này lần lượt có 2/3 method
- Tập method nhỏ đã trở thành idiom đặc trưng của Go
Vòng đường dài quanh generics
- Nhóm Go đã quan sát thấy generic programming có xu hướng khiến người ta nghĩ về type trước algorithm
- Ưu tiên thiết kế hữu cơ hơn abstraction từ sớm
- Coi trọng function hơn container
- Go đưa các generic container như map, slice, array và channel vào ngôn ngữ, nhưng không cung cấp trực tiếp tính generic đó cho người dùng
- Nhóm cho rằng hầu hết tác vụ lập trình đơn giản là đủ với các type này, nhưng cũng có trường hợp không đủ, và ranh giới này gây bất tiện cho một số người dùng
- Vì interface là nền tảng của lập trình Go, mô hình đa hình mới nhất thiết phải hòa hợp tốt với interface
- Đã có nhiều lần thử nghiệm và các triển khai bị dừng lại
- Sau nhiều thảo luận, nhóm cũng nhận được sự hỗ trợ từ các nhà lý thuyết kiểu do Phil Wadler dẫn dắt
- Cuối cùng Go thiết kế generics bằng cách khái quát hóa interface từ “tập method” thành “tập type”
- Thuật ngữ “generics” xuất phát từ đa hình xoay quanh cấu trúc dữ liệu nên không chỉ chính xác parametric polymorphism mà Go cung cấp, nhưng trên thực tế người ta vẫn dùng từ generics
Compiler: ban đầu là C, sau đó là Go
- Việc compiler Go ban đầu được viết bằng C khiến một phần cộng đồng ngôn ngữ không thoải mái
- Có ý kiến cho rằng nên dùng toolkit như LLVM hoặc self-hosting bằng chính Go
- Lý do viết compiler đầu tiên bằng C là thực dụng
- Để bootstrap một ngôn ngữ mới cần một ngôn ngữ hiện có
- Ken Thompson đã viết sẵn một compiler C, và cấu trúc bên trong của nó có thể làm nền tảng cho compiler Go
- Nếu vừa thiết kế ngôn ngữ vừa viết compiler bằng chính ngôn ngữ đó, thiết kế có thể bị lệch về một ngôn ngữ thuận tiện cho việc viết compiler
- Compiler ban đầu tạo code bình thường và phần bên trong cũng không gọn gàng, nhưng nhỏ, quen thuộc và dễ thay đổi nhanh
- Việc thêm segmented stack tự động tăng kích thước cho thấy lợi thế của lựa chọn này
- Nếu dùng toolkit như LLVM, việc tích hợp sẽ khó vì còn phải bao gồm thay đổi ABI và hỗ trợ garbage collector
- Cấu trúc vốn đến từ họ compiler Plan 9 cũng rất phù hợp với cross compile
- Trong Go 1.5, Russ Cox đã viết một công cụ dịch compiler từ C sang Go theo cách bán tự động
- Đến thời điểm này, ngôn ngữ đã hoàn thiện nên lo ngại thiết kế ngôn ngữ bị compiler chi phối đã biến mất
- Sau khi chuyển sang Go, các ưu điểm của Go như testing, tooling, automatic rewriting và phân tích hiệu năng cũng có thể được áp dụng cho phát triển compiler
- Hiện nay cũng có compiler Go dựa trên LLVM và nhiều compiler khác
Vận hành dự án mã nguồn mở và cộng đồng
- Ngay từ đầu, Go đã xác định rằng để thành công thì phải là một dự án mã nguồn mở
- Tuy nhiên, nhóm cho rằng phát triển riêng tư sẽ hiệu quả hơn cho đến khi sắp xếp rõ các ý tưởng cốt lõi và triển khai hành vi
- Hai năm đầu trước khi công bố rất quan trọng để làm rõ mục tiêu
- Sau khi chuyển sang mã nguồn mở, input từ cộng đồng rất lớn; đặc biệt Ian Taylor đã dành nhiều thời gian trả lời câu hỏi
- Bản port Windows hoàn toàn do cộng đồng thực hiện dưới sự hướng dẫn của Alex Brainman
- Nhóm Go mất nhiều thời gian để hiểu cách tốt nhất để cộng tác với cộng đồng
- Họ nghĩ mình giao tiếp tốt, nhưng hiểu nhầm và kỳ vọng lệch nhau đã làm lãng phí thời gian
- Dự án Go chọn cách bảo đảm chất lượng code cao trước khi merge, thay vì nhanh chóng nhận code rồi dọn dẹp sau
- Code review bắt buộc
- Chú ý kỹ lưỡng đến chi tiết
- Cách này đặt thêm gánh nặng lên cộng đồng, nên nếu người tham gia không hiểu giá trị của nó, họ có thể cảm thấy không được chào đón
- Lịch sử repository của dự án Go đã đi qua SVN, Perforce, Mercurial và Git, nhưng Russ Cox đã giữ lại lịch sử nên repository Git hiện nay vẫn còn các bản ghi thay đổi SVN ban đầu
- Google hỗ trợ Go nhưng không đặt agenda cho nhóm Go
- Codebase Go lớn bên trong Google được dùng để test và xác minh các bản phát hành
- Dòng chảy là lấy từ repository công khai vào Google, chứ không phải ngược lại
- Nhóm Go cốt lõi được Google trả lương nhưng hoạt động độc lập
Những thử-sai trong quản lý package
- Bản thân thiết kế package của ngôn ngữ Go rất xuất sắc, nhưng quá trình phát triển quản lý package thì không suôn sẻ
- Việc chỉ định path trong câu lệnh
importbằng chuỗi thông thường là một lựa chọn quan trọng và linh hoạt - Vấn đề là quá trình chuyển từ trạng thái chỉ có thư viện chuẩn sang cách lấy code từ web diễn ra gập ghềnh
- Nhóm Go ban đầu quen với monorepo của Google và cách build từ head
- Họ chưa có đủ kinh nghiệm dùng package manager có nhiều phiên bản package
- Không xử lý độ phức tạp kỹ thuật của việc giải dependency graph ngay từ đầu
- Công việc deps.dev được xem như một dạng bù đắp cho vấn đề này
- Nỗ lực giải quyết quản lý dependency cùng cộng đồng là thiện chí, nhưng khi thiết kế cuối cùng xuất hiện, nhiều thành viên cộng đồng cảm thấy bị gạt ra ngoài
- Trải nghiệm này trở thành bài học về việc sự tham gia của cộng đồng thực sự nên diễn ra như thế nào, và sau đó đã được cải thiện nhiều
- Hiện nay thiết kế đã ổn định, xuất sắc về mặt kỹ thuật và có vẻ hoạt động tốt với hầu hết người dùng
Tài liệu và ví dụ có thể chạy được
- Ban đầu đã có nhiều tài liệu được viết, nhưng khác với mức mà cộng đồng mong muốn
- Phần thiếu nhất là ngay cả những function đơn giản nhất cũng cần ví dụ sử dụng
- Việc cho thấy cách dùng có giá trị hơn việc nói nó làm gì
- Tài liệu hiện nay có nhiều ví dụ, phần lớn do các contributor mã nguồn mở cung cấp
- Go từ sớm đã làm cho các ví dụ có thể chạy trên web
- Trong bài trình bày tại Google I/O 2012, các đoạn code thể hiện concurrency có thể chạy được trong browser
- Andrew Gerrand đã viết công nghệ web giúp điều đó khả thi
- Sau đó nó cũng được triển khai cho blog và tài liệu package online
- The Go playground được cung cấp như một sandbox công khai để mọi người tự do thử nghiệm và phát triển code
Vị trí Go đạt được sau 14 năm
- Nhờ các quyết định trong thiết kế ngôn ngữ và quá trình phát triển, Go đã vượt ra ngoài một ngôn ngữ lập trình đơn thuần để trở thành một cách viết phần mềm
- Các yếu tố sau đã góp phần vào vị trí mà Go đạt được
- Thư viện chuẩn mạnh, triển khai phần lớn chức năng cơ bản cần cho code server
- Concurrency là thành phần hạng nhất của ngôn ngữ
- Cách tiếp cận dựa trên composition thay vì inheritance
- Mô hình packaging làm rõ quản lý dependency
- Công cụ build và test nhanh, tích hợp
- Formatting nghiêm ngặt và nhất quán
- Thái độ coi trọng khả năng đọc hơn sự khéo léo
- Cam kết tương thích
- Cộng đồng Go gồm các Gopher đa dạng và năng động, đóng vai trò then chốt trong thành công của dự án
- Kết quả là code Go trông và hoạt động tương tự nhau bất kể ai viết, có tương đối ít phe phái dùng các tập con ngôn ngữ khác nhau, và được bảo đảm tiếp tục compile và chạy theo thời gian
- Những đặc tính này có thể được xem là kết quả hiếm thấy trong các ngôn ngữ lập trình lớn
1 bình luận
Ý kiến trên Hacker News
Việc các nhân vật chủ chốt dành thời gian viết hồi tưởng là điều rất đáng trân trọng, và rất hữu ích cho những người hiện thật sự muốn tạo ra thay đổi
Tuy vậy, tôi nghĩ Rob Pike chưa nói đủ rõ Go đã làm tốt những gì. Go không chỉ quản lý tốt bản thân ngôn ngữ, mà còn quản lý tốt các lực tác động lên dự án: giới hạn mục đích sử dụng vào lập trình hệ thống, định nghĩa rõ ngôn ngữ và các nguyên tắc, duy trì thái độ ưu tiên chất lượng bằng cách sửa vấn đề trước khi phát hành, kiểm soát chặt chẽ ngôn ngữ, các bản phát hành và thông điệp cốt lõi, đồng thời vẫn chừa chỗ để cộng đồng dẫn dắt ở các mảng phụ
Một điểm ít được đánh giá đúng mức là bản thân Google đã không can thiệp quá nhiều. Có lẽ vì Go thật sự đóng góp vào mục tiêu của Google và có tầm quan trọng. Tôi nghi ngờ liệu các dự án mới ngày nay có còn được như vậy không
Dart gần như không được chấp nhận bên ngoài Flutter, điều này khá thú vị nếu so với thực tế là mã ứng dụng nhiều hơn mã hệ thống rất nhiều. Go là công nghệ then chốt đã chuyển phần mềm phía máy chủ khỏi môi trường Java cồng kềnh sang container native, và đã trở thành nền tảng cho phần lớn hạ tầng ứng dụng web trong 10 năm qua. Trong quá trình tăng trưởng đó, đội ngũ vẫn nhỏ và các nhân vật cốt lõi vẫn ở lại, nhưng không biết sau này có còn như vậy không
Vì vậy, về mặt thiết kế, nó không phù hợp làm ngôn ngữ lập trình hệ thống. Do ban đầu chính họ tự gọi như vậy nên một huyền thoại kỳ lạ đã tồn tại khá lâu, nhưng có vẻ cụm từ đó đã bị gỡ bỏ vài năm trước
Go gần với một đối thủ của họ Java hơn là đối thủ của C hay Rust, và điều đó thể hiện qua phạm vi sử dụng thực tế. Tuy nhiên, nhờ hệ thống kiểu bị giới hạn và hỗ trợ đồng thời xuất sắc, nó thường nằm ở tầng thấp hơn một chút trong stack
Đặc biệt là chuyện quản lý phiên bản. Ban đầu Go không chịu thừa nhận bản thân vấn đề, rồi khi đồng thuận trong cộng đồng lớn dần, họ quên các thảo luận trước đó và quyết định giờ sẽ đi theo modules
Tôi cũng nhớ việc họ không muốn biến thời gian tăng đơn điệu (monotonic time) thành API công khai cho đến khi Cloudflare gặp sự cố do giờ mùa hè
Trong thế giới Java, có niềm vui khi dùng các tính năng mới, chẳng hạn những thứ tuyệt vời như streams, để refactor codebase. Với Go, niềm vui đến từ việc tìm ra abstraction đơn giản và bình thường nhất
Mọi ngôn ngữ đều cần thư viện và framework phù hợp với một lĩnh vực nhất định. Flutter là framework để viết ứng dụng client bằng Dart; nếu không có Flutter, dù bạn thích ngôn ngữ Dart đến mấy, bạn vẫn phải tốn rất nhiều thời gian chỉ để tìm cách vẽ pixel lên màn hình trên Android và iOS
Bất kỳ ai viết ứng dụng cũng đều build trên một stack thư viện và framework. Khác biệt giữa Go và Dart gắn với Flutter chỉ là các thư viện theo lĩnh vực cần cho server, như networking, serialization, encryption, nằm trong thư viện chuẩn của Go
Dart có các thành phần cơ bản như collection và async trong thư viện tích hợp, nhưng các tính năng theo từng lĩnh vực thì phụ thuộc vào package bên ngoài như Flutter. Điều này phần lớn là vì Dart đã có cơ chế quản lý package vững chắc từ rất sớm. Nhiều thư viện cốt lõi do đội Dart viết và bảo trì cũng được phân phối qua trình quản lý package thay vì tích hợp sẵn, và cách đó dễ phát triển hơn nhiều
UI framework thường có vòng đời ngắn hơn ngôn ngữ, nên tôi nghĩ tốt hơn là Flutter không bị gắn cứng vào thư viện chuẩn của Dart. Flutter rất tuyệt, nhưng sẽ không ngạc nhiên nếu 20 năm nữa có thứ gì đó tốt hơn xuất hiện; khi đó nếu Flutter không được tích hợp sẵn, người dùng Dart sẽ dễ chuyển sang hơn
Những thứ FOSS giả như Android thì được dung thứ về mặt chiến lược, nhưng ngoài ra, nếu không dẫn đến lượt nhấp quảng cáo thì bầu không khí chung là xem chúng vô dụng
Năm 2012, khi còn là lập trình viên Python, tôi bắt đầu dùng Go vì phải thao tác bit cho Modbus. Tôi không triển khai đoạn mã đó, nhưng đã sốc vì việc đụng đến bit và byte lại dễ dàng và cứ thế hoạt động
Sau 10 năm và vài lần làm việc với Go gần như toàn thời gian, tôi vẫn ngạc nhiên rằng phần lớn mọi thứ cứ thế hoạt động
Tôi thích ngôn ngữ Go và cộng đồng Go. Tôi biết ơn công việc của những người tạo ra Go như Rob, Ian, Russ, và cũng trân trọng việc bài nói/bài viết này đã thẳng thắn đề cập đến “giai đoạn gập ghềnh” với cộng đồng
Tuy vậy, với tư cách người đã trải qua giai đoạn đó, đặc biệt là sự hỗn loạn về quản lý package, rất khó tin rằng họ thật sự không biết chính xác mình đang hành xử như thế nào. Dù vậy, nhận định trong bài rằng giờ đã đi đến một giải pháp tốt cũng đúng
Tôi hy vọng Go trong 10 năm tới, cùng các ngôn ngữ tương tự được truyền cảm hứng như Zig và Deno, sẽ tiếp tục phát triển thành các cộng đồng lành mạnh
Tôi là nhà sinh học tính toán hơn là lập trình viên nên việc dùng Go khá thất thường, nhưng mỗi khi quay lại Go, mã cũ vẫn biên dịch được và ngôn ngữ hoạt động đúng như kỳ vọng
Đồng thời, tôi đánh giá cao việc Rob Pike không chấp nhận mọi tính năng hào nhoáng, nhưng vẫn thừa nhận những sai lầm trong quá trình học cách tham gia cùng cộng đồng
Vài cái chạy ngay, vài cái phải chỉnh một chút vì mô hình AI biết thông tin API hơi cũ
Dù vậy, việc chỉ cần sao chép chương trình lên server bằng
scplà nó cứ thế chạy thật đáng kinh ngạc, so với việc lần nào cũng phải nhảy điệu “hệ thống venv đang thịnh hành lần này là gì đây” trong PythonBài viết hay, nhưng cũng có vẻ khá tự chúc mừng bản thân. Tôi đã nghĩ sẽ có sự thừa nhận về những vấn đề sâu hơn của ngôn ngữ này, nhưng có lẽ điểm mấu chốt với họ là ngôn ngữ thì hoàn hảo còn tôi mới là vấn đề
Vì vậy rất khó để khuyến nghị Go. Có các vấn đề về hệ thống kiểu, xử lý lỗi, đồng thời không an toàn, cú pháp quá đơn giản,
nil, giá trị 0 mặc định, và nhiều gói phổ biến bị bỏ mặcHiện tôi dùng Rust làm ngôn ngữ chính. Hệ sinh thái sôi động và có tầm nhìn ở nhiều khía cạnh mà Go không có
Nói sắc hơn thì, có vẻ Go đã có thời của nó khi còn cạnh tranh với PHP với tư cách ngôn ngữ backend
Đoạn đầu là “không thích”, đoạn hai là mỉa mai, đoạn ba cũng “không thích”, đoạn bốn là “Rust tốt hơn”, đoạn năm lại mỉa mai
deps.devVài năm sau, ở công ty, tôi đã cố đưa Go vào nhóm để hoàn thành công việc, nhưng một kỹ sư khác đã dành thời gian xem xét ngôn ngữ này và liệt kê lý do vì sao nó không tốt, và anh ấy đã đúng
Điểm mấu chốt là nó đơn giản, nhưng những thứ nhỏ nhặt như xử lý lỗi và import không dùng đến khiến việc sử dụng trở nên đau khổ. Cá nhân tôi thích cách xử lý lỗi, nhưng ghét hệ thống kiểu
Vì trước đây từng dùng OCaml, tôi biết giá trị của các kiểu
OptionvàResultso với try/catch haynil, nhưng tôi dùng Go vì nó dễ. Tuy nhiên sự dễ dàng đó đi kèm chi phí bảo trì dài hạn. Tốt hơn là làm cho đúng ngay từ đầu và không phải đối mặt với khó khăn về sauKhông có ý nói nhẹ chuyện này, nhưng tôi thường nghe Go được mô tả là ngôn ngữ đã lấy 50 năm tiến bộ của ngôn ngữ lập trình rồi vứt bỏ
Không phải theo nghĩa xấu. Rob Pike không bận tâm rằng tính đồng thời không an toàn hay
nillà sai lầm tỷ đô, và phần lớn người dùng Go cũng vậyĐiều đó có làm Go trở thành một ngôn ngữ tốt không? Không. Nhưng đó không phải vấn đề chính. Go là một ngôn ngữ tiện lợi, đủ tốt, kết hợp với các công cụ thuyết phục khiến nó dễ dùng. Nó giống như Crocs của giới ngôn ngữ lập trình
Nếu cần tính toàn vẹn và an toàn cao, tôi sẽ dùng Ada hoặc Ada/SPARK kèm kiểm chứng hình thức. Còn ngoài ra, Go mang lại năng suất cao hơn Rust rất nhiều
Cá nhân tôi xem Rust, giống C++, là ví dụ điển hình của kỹ nghệ quá mức
Nghe có vẻ cay cú, nhưng 10 năm trước tôi đã bị chế giễu trên
go-nutsvà cũng nhận được câu trả lời khinh thường từ Rob Pike. Chỉ vì tôi dám nói rằng cáchgo getvà import module qua mạng vận hành, được quảng bá khắp tài liệu cho người mới, và được cả cộng đồng sử dụng theo cách đó, rốt cuộc là có hại và thiển cậnCây namespace toàn cục kỳ lạ đó có lý ở bên trong, và việc nhấn mạnh các sản phẩm sinh mã đã được check-in cũng dễ hiểu nếu xét việc Google đưa cả artifact build vào monorepo
Tất nhiên vẫn có vấn đề
v2hay vấn đềv0vĩnh viễn. Nhưng vẫn tốt hơn dùng npm, hoặc phải xử lý thứ gì đó của Python khiến người ta muốn làm dấu thánh giáBài này trông giống một phiên bản cá nhân hơn của bài đã đăng trên ACM [1]. Cả hai bài đều thừa nhận rằng họ không tạo ra một ngôn ngữ lập trình mới xuất sắc xét từ góc độ đặc tả ngôn ngữ, mà đã xây dựng rất tốt những thứ xung quanh ngôn ngữ lập trình, và điều đó có thể còn quan trọng hơn
Bài được gửi nói rằng họ đã phát minh ra cách tiếp cận dùng interface và cách tiếp cận concurrency. Nhưng goroutine giống Haskell thread, còn interface, nếu hỗ trợ tham số generic như hiện nay, thì rất giống Haskell typeclass. Xét việc Haskell có trước Go, thật thú vị khi thấy các lập trình viên thủ tục độc lập khám phá sức mạnh của các ý tưởng lập trình hàm
Một đổi mới ngôn ngữ của Go là việc triển khai interface không cần khai báo interface mà nó triển khai. Xét từ góc độ an toàn thì điều này rất tệ, nhưng trên thực tế hiếm khi gây vấn đề, và nó loại bỏ các vấn đề phụ thuộc vòng khủng khiếp gặp trong Haskell và Rust
[1] https://cacm.acm.org/magazines/2022/5/260357-the-go-programm...
Nếu kiểu phải khai báo trước rằng nó triển khai interface nào, Go đã là một ngôn ngữ hoàn toàn khác. Đây từng là một trong những điểm khác biệt chính của Go. Ít nhất là cho đến khi TypeScript đưa ra interface kiểu cấu trúc vì những lý do khác
Có thể nêu ra dù chỉ một ngôn ngữ lập trình đáng để chúng ta quan tâm nào buộc bạn phải cung cấp triển khai cho interface khi định nghĩa interface không?
Có vẻ các vấn đề liên quan đến mật mã của Go vẫn chưa được nhắc đến nhiều. Sau khi OpenSSH loại bỏ SHA1, đội ngũ Go đã mất tới 1 năm mới thêm hỗ trợ SHA2 vào
x/crypto/ssh[1]. Gitea là một nạn nhân nổi tiếng [2]Hơn nữa, việc maintainer mảng mật mã công kích GnuPG [3] và có vẻ cố làm suy giảm uy tín của Dan Bernstein [4] không tạo được sự tin cậy
[1]: https://github.com/golang/go/issues/49269
[2]: https://github.com/go-gitea/gitea/issues/17798
[3]: https://twitter.com/FiloSottile/status/1127643698676797441
[4]: https://twitter.com/FiloSottile/status/1555669786826244096
x/thường bị bỏ bê khá nhiều, và chúng đi theo một mô hình phát triển khiến contributor mới khó gửi patch thành côngNgược lại, tôi xem package crypto trong thư viện chuẩn là một trong những thành công lớn của Go. OpenSSL trong thời gian dài từng là một thảm họa, còn Go, đặc biệt là agl, đã tạo ra và phân phối một implementation thay thế được dùng cực kỳ rộng rãi, với chất lượng trung bình cũng cao. Nhất là xét từ góc độ API công khai có ít “súng tự bắn vào chân” hơn nhiều
Bài viết này nhắc đến
gofmtnhư một điều “đã làm đúng”, và điểm này đặc biệt đáng nhấn mạnhVới nhiều người thiết kế và ủng hộ ngôn ngữ, nó có vẻ nhỏ nhặt, nhưng ngay từ ngày đầu của dự án Go, nó đã loại bỏ hoàn toàn một kiểu tranh luận chuồng xe đạp phổ biến, nên mang lại giá trị rất lớn
Tôi đã thấy khá nhiều ngôn ngữ mới hơn cố ý bắt chước điều này, hoặc chấp nhận nó với suy nghĩ rằng Go có thì chúng ta cũng nên làm
Tôi từng thấy một số quy tắc format khá kỳ lạ, nhưng hầu như không có quy tắc nào là không thể quen được. Ngược lại, trong những codebase không có quy tắc format bắt buộc, rất nhiều lần kết quả là code khó hiểu hơn
go fmtkhiến phần lớn chuyện đó trở nên vô nghĩaTôi đồng ý với nhận định rằng “sự tồn tại của một bộ thư viện vững chắc, được xây dựng tốt, có hầu hết những gì cần để viết mã máy chủ của thế kỷ 21 là một tài sản lớn”
Go được Google tài trợ vì Google có vấn đề cần giải quyết. Họ phải viết rất nhiều mã ứng dụng phía máy chủ, trong khi Python thì quá chậm còn C++ thì quá mong manh. Go rất phù hợp với khoảng trống đó
Một điểm cộng lớn là người của Google đã viết các thư viện cần thiết cho những công việc như vậy và cũng dùng chúng nội bộ. Vì thế khi dùng thư viện, ngay cả các nhánh xử lý lỗi cũng là mã đã được chạy thực tế rất nhiều
Tôi có những bất đồng kỹ thuật về ngôn ngữ này. Đặc biệt là sự nhấn mạnh thời kỳ đầu vào việc dùng hàng đợi cho mọi thứ, thậm chí tạo mutex bằng hàng đợi. Nhưng họ đã nắm đúng green thread cho ca sử dụng của mình. Vấn đề màu hàm là vấn đề thật, còn những mẹo khó hiểu trong Rust để buộc async và thread dùng chung với nhau thì quá mức. Go từ bỏ một chút hiệu năng để đổi lấy sự đơn giản lớn
Sự ác cảm với thread ngày xưa cũng khá thú vị. Tôi bắt đầu với mainframe UNIVAC, nơi đã có thread từ năm 1967 và khi đó chúng được gọi là “activities”. Đến năm 1972, không chỉ có thread ở không gian người dùng mà chúng còn chạy trên bộ xử lý đa nhân đối xứng. Cũng đã có nhập/xuất bất đồng bộ, hàm hoàn tất ở không gian người dùng, lệnh tích hợp cho lock, và hệ điều hành được thread hóa nội bộ
Thread và đa xử lý được xem là chuyện bình thường, và khi chuyển sang UNIX thì có cảm giác như đã mất chúng. UNIX/Linux mất hàng chục năm mới bắt kịp trong lĩnh vực này, và nhiều thế hệ lập trình viên không biết cách dùng bộ xử lý đa nhân với bộ nhớ chia sẻ
Lý thuyết đồng thời thời kỳ đầu của Dijkstra đã được tái phát minh bằng thuật ngữ khác và thường với thiết kế tệ hơn. Người làm Go hiểu bounded buffer của Dijkstra và vì sao bounded buffer có độ dài 0 và 1 lại hữu ích. Thấy điều đó trở lại là điều tốt. Nếu có các thao tác nguyên thủy đúng, lập trình đồng thời không khó đến vậy. Nếu cố giải quyết bằng cách rải lock khắp nơi như lối nghĩ pthreads cổ điển thì sẽ không chạy đúng. Việc bộ lập lịch CPU hồi đầu UNIX/Linux thật sự rất tệ, khiến thao tác bỏ chặn thread hoạt động khủng khiếp, cũng chẳng giúp ích gì
Go được tạo ra vì Rob Pike có vấn đề. Ông không thích C++, Java, Python, và vì ông là Rob Pike nên Google cho phép ông dành vài năm viết một ngôn ngữ mới. Không phải Larry, Sergey, Sundar hay Eric đến gặp Rob để nhờ ông làm gì đó
Nói chung, Go được tài trợ vì Rob Pike xin hỗ trợ, Google muốn giữ Rob Pike ở lại, và họ cho rằng gần như chắc chắn ông sẽ làm ra thứ gì đó thú vị. Trong Google Engineering, tiền của công ty về thực chất gần như là việc cho phép mọi người có thời gian làm việc
Vì Rob Pike là một kỹ sư xuất sắc, Go ở một số mặt đã thành ra khá tốt, và phù hợp với vấn đề ban đầu của ông khi phải dùng C++, Python, Java để viết ứng dụng vận hành của Google. Một số người trong nội bộ Google cũng đồng ý và bắt đầu dùng Go, trong đó có những người có ảnh hưởng đã “khuyến khích” người khác dùng Go thay vì Python
Năm 2009 ở Google, ngoại trừ trường hợp mua lại YouTube, Python hầu như không được dùng cho máy chủ nào ngoài vài tiện ích nội bộ, nên có thể loại bỏ ngay ý tưởng đó. Máy chủ Python lớn nhất do Google tự xây là Mondrian, công cụ review mã do Guido van Rossum viết, sau này được thay thế bằng Critique viết bằng Java
Khi đó Google có chính sách ba ngôn ngữ khá nghiêm ngặt để loại bỏ các cuộc chiến ngôn ngữ lập trình. Có thể dùng C++, Java, Python; Python dùng cho script, C++ cho hạ tầng, Java cho máy chủ web
Cũng có phần chồng lấn. Java được đưa vào muộn hơn C++, và một số máy chủ web vẫn ở lại với C++ vì khó viết lại. Cũng có các nỗ lực chuyển dần sang Java nhưng không thực sự bám rễ. Một số máy chủ hạ tầng như Megastore được viết bằng Java, còn các thư viện lõi nhìn chung là C++ kèm JNI và binding Python
Chính sách cũng có kẽ hở, nên một số nhóm tạo ngôn ngữ riêng cho hạ tầng nội bộ, và trong codebase thực tế cũng có các ngôn ngữ cấu hình tùy chỉnh cùng Sawzall do Rob Pike tạo ra. Cú pháp của nó hơi giống Go
Nhìn chung mọi người có khá nhiều tự do lựa chọn, máy chủ web mới chủ yếu được viết bằng Java, còn những thứ như engine cơ sở dữ liệu mới chủ yếu được viết bằng C++
Việc viết mã bất đồng bộ chắc chắn là khó, và đúng là Go đã giải quyết tốt phần đó. Nhưng Go không phải là dự án do lãnh đạo cấp cao khởi xướng để giải quyết vấn đề của Google. Không phải vì lãnh đạo sợ những dự án như vậy, mà vì có vô số dự án hạ tầng nội bộ được các nhóm liên quan tạo ra và bố trí nhân sự để trực tiếp giải quyết nỗi đau của lập trình viên. Những đợt nâng cấp mạng quy mô lớn được thúc đẩy vào thời đó là ví dụ. Các dự án như vậy đều được thông báo trước
Tôi không nhớ là có sự bất mãn lớn nào về tình trạng đó. Thời gian build là vấn đề cho đến khi Bazel/Blaze hoạt động cùng cụm build từ xa; sau đó mọi thứ đều được cache từ xa nên ngay cả codebase khổng lồ cũng có thể biên dịch trong vài giây. Tốc độ trình biên dịch cục bộ gần như không còn quan trọng, và
javaccũng rất nhanhViệc công bố Go là một điều khá bất ngờ. Nếu nó thật sự được phát triển để giải quyết vấn đề của Google, cách làm thông thường của Google sẽ là phát hành nội bộ trước, phát triển cùng người dùng nội bộ trong vài năm rồi mới công khai. Nhưng Go lại được công khai trước. Những người xung quanh tự hỏi nó dùng để làm gì, một phản ứng khó xảy ra nếu nó thật sự được thúc đẩy bởi nhu cầu nội bộ
Phần nói về quyết định dùng trình biên dịch C của Ken Thompson thay vì LLVM khá thú vị. Mọi người đã phàn nàn, và đặc biệt ở các phiên bản đầu, mã sinh ra cũng kém tối ưu hơn
Đổi lại, họ có thể nhanh chóng tạo ra stack phân đoạn. Nếu phải triển khai trong LLVM và khớp với LLVM ABI thì có thể họ đã không làm được
Trong bài, đây không phải là lợi ích duy nhất của quyết định đó, mà chỉ là một ví dụ về lợi ích
X86FrameLowering đã có hỗ trợ sẵn, và nó cũng đã tồn tại vào thời điểm Go ra mắt. Khi bật chia tách stack, LLVM phát kiểm tra trong phần prologue của hàm, và nếu cần thì gọi
__morestackđể cấp phát thêm stack. Windows MSVC ABI cũng cần mã rất tương tự để hỗ trợ_chkstk, nên nó khớp một cách tự nhiên với hỗ trợ__morestackTrái lại, việc khiến GDB hiểu được stack phân đoạn còn khó hơn bất kỳ phần nào trong triển khai trình biên dịch. Điều này không phụ thuộc vào backend
Có lẽ tác giả đã nhầm với stack có thể di dời. Khi đó việc triển khai khó vì cần thu gom rác chính xác, nhưng hiện nay Azul đã triển khai nó trong LLVM. Cách dễ nhất để triển khai thu gom rác chính xác lúc đó là spill tất cả thanh ghi qua các lời gọi hàm; việc này tốn thêm công triển khai nhưng không đến mức quá đáng. Trình biên dịch Plan 9 dù sao cũng có vẻ làm như vậy, nên có lẽ cũng không phải là hồi quy hiệu năng so với 6g/8g. Dù thế nào, hỗ trợ thu gom rác của Azul hiện đã có một triển khai đúng nghĩa, có thể lưu root trong thanh ghi
Vì vậy, đúng là khả năng thử nghiệm có giá trị, nhưng ví dụ cụ thể này không phải là một trường hợp hoàn hảo
[1]: https://go.dev/doc/go1.3#:~:text=Go%201.3%20has%20changed%20...
Khi phác thảo một ngôn ngữ sở thích có kiểu tĩnh, tôi đang thử một thứ xử lý cả interface lẫn generic, và tự mình cảm nhận rằng Rob nói đúng. Làm cho hai thứ đó ăn khớp với nhau thật sự khó
Dù vậy, tôi nghĩ việc đó đáng làm. Cá nhân tôi thấy sẽ không mấy hứng thú khi dùng một ngôn ngữ kiểu tĩnh mà người dùng không thể tự định nghĩa kiểu generic
Trước đây tôi từng lập trình trong một môi trường BASIC có
GOSUBcho subroutine, nhưng không có cách viết subroutine nhận đối số. Tôi không muốn lặp lại trải nghiệm đó ở cấp độ hệ thống kiểuVì vậy tôi hiểu vì sao nhóm Go mất nhiều thời gian để tìm ra thiết kế tốt. Thiết kế một ngôn ngữ tốt đã khó; thiết kế một ngôn ngữ tốt có hệ thống kiểu khó gấp 10 lần; thiết kế một hệ thống kiểu có generic lại khó hơn thế thêm 10 lần nữa
Có thể nói Go đã chọn phần dễ nhất trong việc triển khai generic. Lý do họ không làm trước đây không phải là kỹ thuật. Đó là vấn đề tư tưởng, hoàn toàn dựa trên sự thiếu hiểu biết, vì phần lớn các nhà thiết kế Go đã không còn theo dõi lĩnh vực lý thuyết ngôn ngữ lập trình từ sau cuối thập niên 90