Go ra đời năm 2009 tại Google, từ một sự bực bội rất cụ thể: build một dịch vụ C++ lớn mất bốn mươi lăm phút. Ba người — Rob Pike, Ken Thompson, Robert Griesemer — bắt đầu phác thảo một ngôn ngữ trong lúc chờ trình biên dịch chạy.

Bài này không kể lịch sử. Nó trả lời câu hỏi thực tế hơn: Go đánh đổi cái gì lấy cái gì, và vì sao đánh đổi đó thắng ở mảng hạ tầng — Docker, Kubernetes, Terraform, Prometheus đều viết bằng Go.

Ba con số

Chương trình Hello World, và tôi đo bằng Go 1.23:

  build lần đầu (cache rỗng) : 1333 ms
  build lại                  :   48 ms
  kích thước binary          :  2,1 MB
  phụ thuộc động             :  not a dynamic executable

Bốn mươi tám mili giây. Đó là lý do tồn tại của ngôn ngữ này, và mọi quyết định thiết kế khác đều xoay quanh việc giữ con số đó nhỏ.

Dòng cuối quan trọng không kém: binary không phụ thuộc thư viện động nào. Không cần cài runtime, không cần JRE, không cần python3, không cần libc đúng phiên bản. Chép một tệp lên máy chủ và chạy.

Đây chính là thứ làm Go thắng trong container: bài 97 của sê-ri Java đo được ảnh Docker nhỏ nhất cho một ứng dụng Java là 126 MB. Với Go, ảnh chứa đúng một binary 2,1 MB trên scratch — và ta sẽ đo nó ở bài 55.

Cross-compile là một biến môi trường

  windows/amd64   2.2M
  darwin/arm64    2.2M
  linux/386       2.0M

Ba dòng trên chạy trên cùng một máy Linux ARM, không cài thêm gì:

GOOS=windows GOARCH=amd64 go build -o app.exe

Không toolchain riêng, không máy ảo, không CI đa nền tảng. Đây là thứ khiến các công cụ dòng lệnh hay được viết bằng Go: tác giả build một lần ra mười nền tảng.

Cái giá phải trả

Không có gì miễn phí. Go đổi rất nhiều thứ để có bốn mươi tám mili giây kia.

Ngôn ngữ cố tình nhỏ. Hai mươi lăm từ khoá, so với năm mươi mấy của Java. Không nạp chồng toán tử, không nạp chồng phương thức, không giá trị mặc định cho tham số, không kế thừa. Nhiều thứ bạn quen ở Java hay Python đơn giản là không có.

Mã dài hơn. Xử lý lỗi bằng if err != nil lặp đi lặp lại là thứ mọi người mới học đều phàn nàn. Ta sẽ nói kỹ ở bài 23 — có lý do cho nó, nhưng lý do đó không làm mã ngắn lại.

Generics đến muộn. Mãi tới Go 1.18 (2022) mới có, và vẫn hạn chế hơn Java. Bài 16 sẽ đo xem nó làm được gì và không làm được gì.

Binary lớn cho chương trình nhỏ. 2,1 MB cho Hello World, vì runtime của Go — bộ lập lịch goroutine, bộ thu gom rác — nằm luôn trong đó. Đổi lại là dòng "not a dynamic executable" ở trên.

Go hợp và không hợp với việc gì

Hợp: dịch vụ mạng và API, công cụ dòng lệnh, hạ tầng và DevOps, thứ gì cần nhiều kết nối đồng thời, và bất cứ khi nào "chép một tệp lên là chạy" có giá trị.

Không hợp: giao diện người dùng, tính toán khoa học và học máy (hệ sinh thái Python vượt xa), miền nghiệp vụ rất phức tạp cần mô hình hoá sâu (hệ thống kiểu của Go hơi nghèo cho việc đó), và những dự án mà thư viện sẵn có chỉ tồn tại ở nền tảng khác.

Một cách nói tôi thấy đúng: Go được thiết kế cho đội đông người viết dịch vụ, không phải cho cá nhân viết mã tinh xảo. Cú pháp nghèo là có chủ đích — nó làm mã của người khác dễ đọc, và làm số cách viết một việc giảm xuống còn một.

Ba thứ Go làm khác hẳn

Ba điều này sẽ chiếm phần lớn sê-ri, nên tôi nêu trước để bạn biết cái gì đang tới:

Đồng thời nằm trong ngôn ngữ. go f() khởi động một goroutine, và channel là kiểu dữ liệu có sẵn với cú pháp riêng. Không phải thư viện — là từ khoá. Bài 31 tới 42 nói về chuyện này, và tôi sẽ đo chi phí thật của một goroutine.

Lỗi là giá trị, không phải ngoại lệ. Không có try/catch. Hàm trả về hai giá trị, cái thứ hai là error, và bạn kiểm nó. Bài 23.

Interface được thoả mãn ngầm định. Không có implements. Một kiểu thoả mãn interface chỉ vì nó có đủ phương thức. Bài 13 — và đây là ý tưởng tôi thấy hay nhất của ngôn ngữ.

Về sê-ri này

Sáu mươi bài, đi từ cú pháp tới đưa dịch vụ lên sản xuất. Nguyên tắc giống sê-ri Java trước đó: không viết điều chưa chạy thử. Mọi con số trong sê-ri đều đo được, và khi phép đo đi ngược lời khuyên phổ biến, tôi sẽ nói rõ.

Bạn cần Go 1.21 trở lên. Bài sau sẽ cài.

Thử ba mươi giây

Nếu máy bạn đã có Go:

echo 'package main
import "fmt"
func main(){ fmt.Println("chào") }' > /tmp/c.go
cd /tmp && go mod init tmp 2>/dev/null; time go build -o c c.go && ls -lh c

Con số real in ra và kích thước tệp là hai thứ định nghĩa Go. Nếu chúng làm bạn thấy đáng, sê-ri này dành cho bạn.

Ngày mai: cài Go cho tử tế, và hiểu go run khác go build chỗ nào.