Bản phân phối Go bao gồm một câu lệnh tên là “go”, tự động hoá việc tải về, build, cài đặt và kiểm thử các package cũng như các lệnh viết bằng Go. Tài liệu này nói về lý do chúng tôi viết một lệnh mới, nó là gì, nó không phải là gì, và cách dùng nó.
Động lực
Có thể bạn đã xem những buổi nói chuyện đầu tiên về Go, trong đó Rob Pike nói đùa rằng ý tưởng về Go nảy sinh trong lúc chờ một server lớn của Google biên dịch xong. Đó thực sự là động lực của Go: xây dựng một ngôn ngữ hoạt động tốt cho việc phát triển những phần mềm lớn mà Google viết và vận hành. Ngay từ đầu đã rõ rằng một ngôn ngữ như vậy phải cung cấp cách diễn đạt rành mạch các phụ thuộc giữa các thư viện mã nguồn, do đó mới có cách nhóm theo package và các khối import tường minh. Cũng rõ ngay từ đầu rằng bạn có thể muốn một cú pháp tuỳ ý để mô tả mã nguồn đang được import; đó là lý do import path là các string literal.
Một mục tiêu tường minh của Go ngay từ đầu là có thể build mã nguồn Go chỉ bằng thông tin nằm trong chính mã nguồn, không cần viết một makefile hay một trong vô số thứ hiện đại thay thế cho makefile. Nếu Go cần một tệp cấu hình để giải thích cách build chương trình của bạn, thì Go đã thất bại.
Ban đầu chưa có compiler cho Go, và quá trình phát triển lúc đầu tập trung vào việc xây dựng compiler rồi xây dựng các thư viện cho nó. Vì tiện lợi, chúng tôi hoãn việc tự động hoá quá trình build mã Go và dùng make cùng các makefile viết tay. Khi việc biên dịch một package đơn lẻ cần gọi compiler của Go nhiều lần, chúng tôi thậm chí đã dùng một chương trình để viết makefile giúp mình. Bạn có thể tìm thấy nó nếu đào sâu vào lịch sử repository.
Mục đích của lệnh go mới là quay lại với lý tưởng đó, rằng các chương trình Go nên biên dịch được mà không cần cấu hình hay công sức bổ sung nào từ phía lập trình viên, ngoài việc viết các câu lệnh import cần thiết.
Cấu hình so với quy ước
Cách để đạt được sự đơn giản của một hệ thống không cần cấu hình là thiết lập các quy ước. Hệ thống chỉ hoạt động chừng nào các quy ước đó còn được tuân theo. Khi chúng tôi mới ra mắt Go, nhiều người xuất bản package phải được cài đặt ở những chỗ nhất định, dưới những tên nhất định, dùng những công cụ build nhất định, thì mới dùng được. Điều đó dễ hiểu: hầu hết các ngôn ngữ khác đều làm vậy. Trong vài năm qua chúng tôi đã liên tục nhắc mọi người về lệnh goinstall (nay được thay bằng go get) cùng các quy ước của nó: thứ nhất, import path được suy ra theo một cách đã biết từ URL của mã nguồn; thứ hai, nơi lưu mã nguồn trên hệ thống tệp cục bộ được suy ra theo một cách đã biết từ import path; thứ ba, mỗi thư mục trong cây mã nguồn tương ứng với đúng một package; và thứ tư, package được build chỉ bằng thông tin có trong mã nguồn. Ngày nay, đại đa số package tuân theo những quy ước này. Nhờ vậy hệ sinh thái Go đơn giản hơn và mạnh mẽ hơn.
Chúng tôi đã nhận được nhiều yêu cầu cho phép đặt một makefile trong thư mục package để cung cấp thêm chút cấu hình ngoài những gì có trong mã nguồn. Nhưng điều đó sẽ đưa vào những quy tắc mới. Vì chúng tôi không nhượng bộ những yêu cầu như vậy, chúng tôi đã có thể viết lệnh go và loại bỏ hoàn toàn việc dùng make hay bất kỳ hệ thống build nào khác.
Cần hiểu rõ rằng lệnh go không phải là một công cụ build đa dụng. Nó không thể cấu hình được và nó không cố build bất cứ thứ gì ngoài các package Go. Đây là những giả định đơn giản hoá quan trọng: chúng không chỉ đơn giản hoá phần hiện thực mà quan trọng hơn là đơn giản hoá chính việc sử dụng công cụ.
Các quy ước của Go
Lệnh go yêu cầu mã nguồn tuân theo vài quy ước then chốt, đã được thiết lập vững chắc.
Thứ nhất, import path được suy ra theo một cách đã biết từ URL của mã nguồn. Với Bitbucket, GitHub, Google Code và Launchpad, thư mục gốc của repository được xác định bằng URL chính của repository, bỏ đi tiền tố https://. Các thư mục con được đặt tên bằng cách nối thêm vào đường dẫn đó. Ví dụ, mã nguồn của package logging glog của Google được lấy về bằng cách chạy
git clone https://github.com/golang/glog
và do đó import path của package glog là “github.com/golang/glog”.
Những đường dẫn này hơi dài, nhưng đổi lại chúng ta có được một không gian tên được quản lý tự động cho các import path, cùng khả năng để một công cụ như lệnh go nhìn vào một import path xa lạ và suy ra nơi lấy mã nguồn.
Thứ hai, nơi lưu mã nguồn trên hệ thống tệp cục bộ được suy ra theo một cách đã biết từ import path, cụ thể là $GOPATH/src/<import-path>. Nếu chưa được đặt, $GOPATH mặc định là một thư mục con tên go trong thư mục home của người dùng. Nếu $GOPATH được đặt thành một danh sách đường dẫn, lệnh go sẽ thử <dir>/src/<import-path> với từng thư mục trong danh sách đó.
Theo quy ước, mỗi cây thư mục đó chứa một thư mục cấp cao nhất tên là “bin” để chứa các tệp thực thi đã biên dịch, một thư mục cấp cao nhất tên “pkg” để chứa các package đã biên dịch có thể import được, và thư mục “src” để chứa các tệp mã nguồn của package. Áp đặt cấu trúc này cho phép chúng tôi giữ mỗi cây thư mục đó khép kín: dạng đã biên dịch và mã nguồn luôn nằm gần nhau.
Những quy ước đặt tên này cũng cho phép chúng ta đi theo chiều ngược lại, từ tên thư mục ra import path của nó. Ánh xạ này quan trọng với nhiều lệnh con của lệnh go, như chúng ta sẽ thấy dưới đây.
Thứ ba, mỗi thư mục trong cây mã nguồn tương ứng với đúng một package. Bằng cách giới hạn một thư mục chỉ chứa một package, chúng ta không phải tạo ra những import path lai vừa chỉ ra thư mục vừa chỉ ra package bên trong thư mục đó. Ngoài ra, hầu hết các công cụ quản lý tệp và giao diện người dùng đều làm việc trên thư mục như đơn vị cơ bản. Gắn đơn vị cơ bản của Go—package—với cấu trúc hệ thống tệp nghĩa là các công cụ hệ thống tệp trở thành công cụ làm việc với package Go. Sao chép, di chuyển hay xoá một package tương ứng với sao chép, di chuyển hay xoá một thư mục.
Thứ tư, mỗi package được build chỉ bằng thông tin có trong các tệp mã nguồn. Điều này làm tăng khả năng công cụ thích ứng được với các môi trường và điều kiện build thay đổi. Ví dụ, nếu chúng tôi cho phép thêm cấu hình như cờ cho compiler hay các công thức dòng lệnh, thì cấu hình đó sẽ phải được cập nhật mỗi khi các công cụ build thay đổi; nó cũng sẽ gắn chặt về bản chất với việc dùng một toolchain cụ thể.
Bắt đầu với lệnh go
Cuối cùng, một vòng dạo nhanh về cách dùng lệnh go. Như đã nói ở trên, giá trị mặc định của $GOPATH trên Unix là $HOME/go. Chúng ta sẽ lưu chương trình của mình ở đó. Để dùng một vị trí khác, bạn có thể đặt $GOPATH; xem Cách viết mã Go để biết chi tiết.
Trước hết chúng ta thêm một ít mã nguồn. Giả sử ta muốn dùng thư viện đánh chỉ mục từ dự án codesearch cùng với một cây đỏ-đen nghiêng trái. Ta có thể cài cả hai bằng lệnh con “go get”:
$ go get github.com/google/codesearch/index
$ go get github.com/petar/GoLLRB/llrb
$
Cả hai dự án này giờ đã được tải về và cài vào $HOME/go, nơi chứa hai thư mục src/github.com/google/codesearch/index/ và src/github.com/petar/GoLLRB/llrb/, cùng với các package đã biên dịch (trong pkg/) của những thư viện đó và các phụ thuộc của chúng.
Vì chúng ta đã dùng hệ thống quản lý phiên bản (Mercurial và Git) để lấy mã nguồn về, cây mã nguồn cũng chứa các tệp khác trong những repository tương ứng, chẳng hạn các package liên quan. Lệnh con “go list” liệt kê các import path tương ứng với các đối số của nó, và mẫu “./...” nghĩa là bắt đầu từ thư mục hiện tại (“./”) và tìm mọi package bên dưới thư mục đó (“...”):
$ cd $HOME/go/src
$ go list ./...
github.com/google/codesearch/cmd/cgrep
github.com/google/codesearch/cmd/cindex
github.com/google/codesearch/cmd/csearch
github.com/google/codesearch/index
github.com/google/codesearch/regexp
github.com/google/codesearch/sparse
github.com/petar/GoLLRB/example
github.com/petar/GoLLRB/llrb
$
Chúng ta cũng có thể test những package đó:
$ go test ./...
? github.com/google/codesearch/cmd/cgrep [no test files]
? github.com/google/codesearch/cmd/cindex [no test files]
? github.com/google/codesearch/cmd/csearch [no test files]
ok github.com/google/codesearch/index 0.203s
ok github.com/google/codesearch/regexp 0.017s
? github.com/google/codesearch/sparse [no test files]
? github.com/petar/GoLLRB/example [no test files]
ok github.com/petar/GoLLRB/llrb 0.231s
$
Nếu một lệnh con của go được gọi mà không liệt kê đường dẫn nào, nó làm việc trên thư mục hiện tại:
$ cd github.com/google/codesearch/regexp
$ go list
github.com/google/codesearch/regexp
$ go test -v
=== RUN TestNstateEnc
--- PASS: TestNstateEnc (0.00s)
=== RUN TestMatch
--- PASS: TestMatch (0.00s)
=== RUN TestGrep
--- PASS: TestGrep (0.00s)
PASS
ok github.com/google/codesearch/regexp 0.018s
$ go install
$
Lệnh con “go install” đó cài bản mới nhất của package vào thư mục pkg. Vì lệnh go có thể phân tích đồ thị phụ thuộc, “go install” cũng cài mọi package mà package này import nhưng đã lỗi thời, một cách đệ quy.
Để ý rằng “go install” đã xác định được tên import path của package trong thư mục hiện tại, nhờ quy ước đặt tên thư mục. Sẽ tiện hơn một chút nếu chúng ta được tự chọn tên thư mục lưu mã nguồn, và hẳn chúng ta sẽ không chọn một cái tên dài như vậy, nhưng khả năng đó sẽ đòi hỏi thêm cấu hình và độ phức tạp trong công cụ. Gõ thêm một hai tên thư mục là cái giá nhỏ phải trả để đổi lấy sự đơn giản và sức mạnh gia tăng.
Giới hạn
Như đã nói ở trên, lệnh go không phải là một công cụ build đa dụng. Cụ thể, nó không có phương tiện nào để sinh ra các tệp mã nguồn Go trong lúc build, mặc dù nó có cung cấp go generate, lệnh này có thể tự động hoá việc tạo các tệp Go trước khi build. Với các thiết lập build phức tạp hơn, bạn có thể cần viết một makefile (hoặc một tệp cấu hình cho công cụ build bạn chọn) để chạy công cụ sinh ra các tệp Go, rồi commit những tệp mã nguồn được sinh ra đó vào repository của bạn. Việc này tốn công hơn cho bạn, tác giả package, nhưng tốn công ít hơn đáng kể cho người dùng của bạn, những người có thể dùng “go get” mà không cần lấy về và build thêm bất kỳ công cụ nào.
Thông tin thêm
Để biết thêm, hãy đọc Cách viết mã Go và xem tài liệu về lệnh go.