Bài này cài Go và chạy chương trình đầu tiên. Phần đáng đọc nằm ở cuối: khác biệt giữa go run và go build không phải chuyện phong cách, và số đo cho thấy vì sao.
Cài
macOS: brew install go. Linux: tải từ go.dev/dl, giải nén vào /usr/local, thêm /usr/local/go/bin vào PATH. Windows: tải bộ cài .msi.
Kiểm:
$ go version
go version go1.23.12 linux/arm64
Cần Go 1.21 trở lên cho sê-ri này — slog, min/max dựng sẵn và vài thứ khác cần bản đó.
Một điểm khác Java đáng nói ngay: Go không có khái niệm "JDK và JRE riêng". Một bản cài, vừa biên dịch vừa chạy. Và bạn cũng không cần cài Go trên máy chủ — bài 1 đã đo: binary tĩnh, chép lên là chạy.
Đừng đụng vào GOPATH nữa. Nó là cách tổ chức mã từ trước 2018 và mọi hướng dẫn nhắc tới nó đều đã cũ.
Chương trình đầu tiên
package main
import "fmt"
func main() {
fmt.Println("Xin chào từ Go")
}
Bốn thứ cần biết ngay từ bốn dòng này:
package main là bắt buộc cho chương trình chạy được. Mọi tệp Go đều thuộc một package, và package tên main là package đặc biệt: nó sinh ra binary. Package tên khác thì sinh ra thư viện.
func main() là điểm vào, không tham số, không giá trị trả về. Muốn đọc tham số dòng lệnh thì dùng os.Args; muốn trả mã thoát thì os.Exit.
import phải được dùng. Import một package rồi không dùng là lỗi biên dịch, không phải cảnh báo. Điều này làm nhiều người mới bực, và nó là ví dụ đầu tiên của triết lý Go: thà khó chịu lúc viết còn hơn để mã rác tích tụ.
Không có dấu chấm phẩy. Trình biên dịch tự chèn, nên bạn không gõ. Nhưng nó cũng bắt bạn đặt dấu { cùng dòng với func — xuống dòng là lỗi.
gofmt không phải tuỳ chọn
gofmt -w main.go # hoặc: go fmt ./...
Go có một định dạng chuẩn, do công cụ áp đặt, không cấu hình được. Thụt lề bằng tab, khoảng trắng ở đâu, xuống dòng thế nào — tất cả cố định.
Nghe độc đoán, nhưng nó xoá sổ hẳn một loại tranh cãi trong đội, và làm mã của mọi dự án Go trên thế giới trông như nhau. Với tôi đây là một trong những quyết định đúng nhất của ngôn ngữ.
Trình soạn thảo của bạn nên chạy gofmt khi lưu. VS Code với tiện ích Go làm sẵn.
go run khác go build chỗ nào
Đây là phần có số đo.
go run main.go (3 lần) : 1492 ms | 62 ms | 74 ms
./app (3 lần) : 1 ms | 1 ms | 1 ms
Chênh sáu mươi lần, và lý do rất đơn giản: go run biên dịch rồi mới chạy, mỗi lần. Nó dựng một binary tạm trong thư mục cache, chạy nó, rồi xoá đi.
Lần đầu 1492 ms vì cache còn rỗng. Từ lần thứ hai, Go dùng lại kết quả biên dịch nên xuống còn 62 ms — nhưng vẫn phải liên kết và khởi động lại.
Nên dùng cái nào:
go run khi đang viết và thử — gõ một lệnh, thấy kết quả ngay.
go build khi đo hiệu năng, khi chạy nhiều lần, và luôn luôn khi đem lên máy chủ. Đừng bao giờ chạy go run trong sản xuất: bạn cần Go cài trên máy đó, cần mã nguồn nằm đó, và trả 60 ms mỗi lần khởi động.
Một biến thể hay dùng: go install biên dịch rồi đặt binary vào $GOPATH/bin (mặc định ~/go/bin) — tiện cho công cụ dòng lệnh bạn muốn gọi từ mọi nơi.
Bốn lệnh dùng hằng ngày
go run . # chạy package trong thư mục hiện tại
go build ./... # build mọi package
go test ./... # chạy mọi test
go vet ./... # tìm lỗi mà trình biên dịch không bắt
go vet đáng chạy thường xuyên. Nó bắt những thứ hợp lệ về cú pháp nhưng gần như chắc chắn sai — chuỗi định dạng không khớp đối số, khoá bị sao chép, so sánh vô nghĩa. Rẻ và hay đúng.
Dấu ./... nghĩa là "package này và mọi package con". Bạn sẽ gõ nó rất nhiều.
Thử ba mươi giây
mkdir /tmp/thu && cd /tmp/thu
go mod init thu
printf 'package main\nimport "fmt"\nimport "os"\nfunc main(){ fmt.Println("chào", os.Args) }\n' > main.go
go run . mot hai ba
Rồi thử xoá dòng import "os" mà vẫn giữ os.Args, và ngược lại — giữ import mà bỏ chỗ dùng. Cả hai đều là lỗi biên dịch, và ba mươi giây đó cho bạn thấy Go nghiêm khắc tới mức nào với những thứ Java chỉ cảnh báo.
Ngày mai: package, module và go.mod — cách Go tổ chức mã và quản lý phụ thuộc.