Mục lục:
Package
Lệnh
Kiểu dữ liệu
Hàm
Hằng số
Biến
Cú pháp
Những lỗi và cạm bẫy thường gặp
“Doc comment” là những chú thích nằm ngay trước các khai báo package, const, func, type và var ở mức top-level, không có dòng trống xen giữa. Mọi tên được export (viết hoa chữ cái đầu) đều nên có doc comment.
Hai package go/doc và go/doc/comment cung cấp khả năng trích xuất tài liệu từ mã nguồn Go, và nhiều công cụ khác nhau tận dụng tính năng này. Lệnh go doc tra cứu và in ra doc comment của một package hoặc một symbol cho trước. (Symbol là một const, func, type hoặc var ở mức top-level.) Máy chủ web pkg.go.dev hiển thị tài liệu cho các package Go công khai (khi giấy phép của chúng cho phép điều đó). Chương trình phục vụ trang đó là golang.org/x/pkgsite/cmd/pkgsite, nó cũng có thể chạy cục bộ để xem tài liệu của các module riêng tư hoặc khi không có kết nối internet. Language server gopls cung cấp tài liệu khi bạn soạn thảo tệp mã nguồn Go trong IDE.
Phần còn lại của trang này mô tả cách viết doc comment trong Go.
Package
Mỗi package nên có một package comment giới thiệu về package đó. Nó cung cấp thông tin liên quan tới toàn bộ package và nhìn chung thiết lập kỳ vọng cho package. Đặc biệt với các package lớn, sẽ rất hữu ích nếu package comment đưa ra một cái nhìn tổng quan ngắn gọn về những phần quan trọng nhất của API, kèm link tới các doc comment khác khi cần.
Nếu package đơn giản, package comment có thể ngắn gọn. Ví dụ:
// Package path implements utility routines for manipulating slash-separated
// paths.
//
// The path package should only be used for paths separated by forward
// slashes, such as the paths in URLs. This package does not deal with
// Windows paths with drive letters or backslashes; to manipulate
// operating system paths, use the [path/filepath] package.
package path
Dấu ngoặc vuông trong [path/filepath] tạo ra một documentation link.
Như có thể thấy trong ví dụ này, doc comment trong Go dùng câu hoàn chỉnh. Với package comment, điều đó nghĩa là câu đầu tiên bắt đầu bằng “Package ”.
Với package gồm nhiều tệp, package comment chỉ nên nằm trong một tệp mã nguồn. Nếu nhiều tệp cùng có package comment, chúng sẽ được nối lại thành một chú thích lớn cho toàn bộ package.
Lệnh
Package comment của một lệnh cũng tương tự, nhưng nó mô tả hành vi của chương trình thay vì mô tả các symbol Go trong package. Theo quy ước, câu đầu tiên bắt đầu bằng chính tên của chương trình, viết hoa vì nó nằm ở đầu câu. Ví dụ, đây là bản rút gọn của package comment cho gofmt:
/*
Gofmt formats Go programs.
It uses tabs for indentation and blanks for alignment.
Alignment assumes that an editor is using a fixed-width font.
Without an explicit path, it processes the standard input. Given a file,
it operates on that file; given a directory, it operates on all .go files in
that directory, recursively. (Files starting with a period are ignored.)
By default, gofmt prints the reformatted sources to standard output.
Usage:
gofmt [flags] [path ...]
The flags are:
-d
Do not print reformatted sources to standard output.
If a file's formatting is different than gofmt's, print diffs
to standard output.
-w
Do not print reformatted sources to standard output.
If a file's formatting is different from gofmt's, overwrite it
with gofmt's version. If an error occurred during overwriting,
the original file is restored from an automatic backup.
When gofmt reads from standard input, it accepts either a full Go program
or a program fragment. A program fragment must be a syntactically
valid declaration list, statement list, or expression. When formatting
such a fragment, gofmt preserves leading indentation as well as leading
and trailing spaces, so that individual sections of a Go program can be
formatted by piping them through gofmt.
*/
package main
Phần đầu của chú thích được viết theo kiểu semantic linefeeds, trong đó mỗi câu mới hoặc mỗi cụm từ dài nằm riêng trên một dòng, điều này giúp đọc diff dễ hơn khi mã nguồn và chú thích thay đổi. Các đoạn văn phía sau tình cờ không theo quy ước này và được ngắt dòng thủ công. Cách nào tốt nhất cho mã nguồn của bạn thì cứ dùng cách đó. Dù thế nào đi nữa, go doc và pkgsite vẫn ngắt dòng lại nội dung doc comment khi in ra. Ví dụ:
$ go doc gofmt
Gofmt formats Go programs. It uses tabs for indentation and blanks for
alignment. Alignment assumes that an editor is using a fixed-width font.
Without an explicit path, it processes the standard input. Given a file, it
operates on that file; given a directory, it operates on all .go files in that
directory, recursively. (Files starting with a period are ignored.) By default,
gofmt prints the reformatted sources to standard output.
Usage:
gofmt [flags] [path ...]
The flags are:
-d
Do not print reformatted sources to standard output.
If a file's formatting is different than gofmt's, print diffs
to standard output.
...
Các dòng được thụt lề được xem là văn bản định dạng sẵn: chúng không bị ngắt dòng lại và được in bằng font code trong bản trình bày HTML và Markdown. (Mục Cú pháp bên dưới trình bày chi tiết.)
Kiểu dữ liệu
Doc comment của một kiểu dữ liệu nên giải thích mỗi giá trị thuộc kiểu đó biểu diễn hoặc cung cấp điều gì. Nếu API đơn giản, doc comment có thể khá ngắn. Ví dụ:
package zip
// A Reader serves content from a ZIP archive.
type Reader struct {
...
}
Theo mặc định, lập trình viên nên giả định rằng một kiểu dữ liệu chỉ an toàn khi dùng bởi một goroutine tại một thời điểm. Nếu một kiểu dữ liệu đưa ra bảo đảm mạnh hơn, doc comment nên nói rõ điều đó. Ví dụ:
package regexp
// Regexp is the representation of a compiled regular expression.
// A Regexp is safe for concurrent use by multiple goroutines,
// except for configuration methods, such as Longest.
type Regexp struct {
...
}
Các kiểu dữ liệu trong Go cũng nên hướng tới việc làm cho giá trị zero có ý nghĩa hữu ích. Nếu ý nghĩa đó không hiển nhiên, nó nên được ghi lại trong tài liệu. Ví dụ:
package bytes
// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
...
}
Với một struct có các field được export, hoặc doc comment hoặc các chú thích riêng cho từng field phải giải thích ý nghĩa của mỗi field được export. Ví dụ, doc comment của kiểu dữ liệu này giải thích các field:
package io
// A LimitedReader reads from R but limits the amount of
// data returned to just N bytes. Each call to Read
// updates N to reflect the new amount remaining.
// Read returns EOF when N <= 0.
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
Ngược lại, doc comment của kiểu dữ liệu này để phần giải thích cho các chú thích của từng field:
package comment
// A Printer is a doc comment printer.
// The fields in the struct can be filled in before calling
// any of the printing methods
// in order to customize the details of the printing process.
type Printer struct {
// HeadingLevel is the nesting level used for
// HTML and Markdown headings.
// If HeadingLevel is zero, it defaults to level 3,
// meaning to use <h3> and ###.
HeadingLevel int
...
}
Cũng như với package (ở trên) và hàm (ở dưới), doc comment của kiểu dữ liệu bắt đầu bằng những câu hoàn chỉnh có nêu tên symbol được khai báo. Một chủ ngữ rõ ràng thường làm cho cách diễn đạt sáng sủa hơn, và làm cho văn bản dễ tìm kiếm hơn, dù là trên trang web hay trên dòng lệnh. Ví dụ:
$ go doc -all regexp | grep pairs
pairs within the input string: result[2*n:2*n+2] identifies the indexes
FindReaderSubmatchIndex returns a slice holding the index pairs identifying
FindStringSubmatchIndex returns a slice holding the index pairs identifying
FindSubmatchIndex returns a slice holding the index pairs identifying the
$
Hàm
Doc comment của một hàm nên giải thích hàm trả về cái gì, hoặc, với các hàm được gọi vì tác dụng phụ, thì nó làm gì. Các tham số và giá trị trả về có tên có thể được nhắc tới trực tiếp trong chú thích, không cần cú pháp đặc biệt nào như dấu backquote. (Một hệ quả của quy ước này là những cái tên như a, vốn có thể bị nhầm với từ thông thường, thường được tránh dùng.) Ví dụ:
package strconv
// Quote returns a double-quoted Go string literal representing s.
// The returned string uses Go escape sequences (\t, \n, \xFF, \u0100)
// for control characters and non-printable characters as defined by IsPrint.
func Quote(s string) string {
...
}
Và:
package os
// Exit causes the current program to exit with the given status code.
// Conventionally, code zero indicates success, non-zero an error.
// The program terminates immediately; deferred functions are not run.
//
// For portability, the status code should be in the range [0, 125].
func Exit(code int) {
...
}
Doc comment thường dùng cụm từ “reports whether” để mô tả các hàm trả về giá trị boolean. Cụm “or not” là không cần thiết. Ví dụ:
package strings
// HasPrefix reports whether the string s begins with prefix.
func HasPrefix(s, prefix string) bool
Nếu một doc comment cần giải thích nhiều giá trị trả về, việc đặt tên cho các giá trị trả về có thể làm doc comment dễ hiểu hơn, kể cả khi những cái tên đó không được dùng trong thân hàm. Ví dụ:
package io
// Copy copies from src to dst until either EOF is reached
// on src or an error occurs. It returns the total number of bytes
// written and the first error encountered while copying, if any.
//
// A successful Copy returns err == nil, not err == EOF.
// Because Copy is defined to read from src until EOF, it does
// not treat an EOF from Read as an error to be reported.
func Copy(dst Writer, src Reader) (n int64, err error) {
...
}
Ngược lại, khi các giá trị trả về không cần được gọi tên trong doc comment, chúng thường cũng được lược bỏ luôn trong mã, như trong ví dụ Quote ở trên, để tránh làm rối phần trình bày.
Tất cả các quy tắc này đều áp dụng cho cả hàm thông thường lẫn method. Với method, việc dùng cùng một tên receiver giúp tránh sự khác biệt không cần thiết khi liệt kê toàn bộ các method của một kiểu dữ liệu:
$ go doc bytes.Buffer
package bytes // import "bytes"
type Buffer struct {
// Has unexported fields.
}
A Buffer is a variable-sized buffer of bytes with Read and Write methods.
The zero value for Buffer is an empty buffer ready to use.
func NewBuffer(buf []byte) *Buffer
func NewBufferString(s string) *Buffer
func (b *Buffer) Bytes() []byte
func (b *Buffer) Cap() int
func (b *Buffer) Grow(n int)
func (b *Buffer) Len() int
func (b *Buffer) Next(n int) []byte
func (b *Buffer) Read(p []byte) (n int, err error)
func (b *Buffer) ReadByte() (byte, error)
...
Ví dụ này cũng cho thấy rằng các hàm top-level trả về một kiểu dữ liệu T hoặc con trỏ *T, có thể kèm thêm một giá trị lỗi, sẽ được hiển thị cạnh kiểu dữ liệu T và các method của nó, với giả định rằng chúng là hàm khởi tạo của T.
Theo mặc định, lập trình viên có thể giả định rằng một hàm top-level là an toàn khi gọi từ nhiều goroutine; điều này không cần nêu ra một cách tường minh.
Mặt khác, như đã nói ở mục trước, việc sử dụng một giá trị thuộc một kiểu dữ liệu theo bất kỳ cách nào, kể cả gọi method, thường được giả định là chỉ giới hạn cho một goroutine tại một thời điểm. Nếu những method an toàn khi dùng đồng thời chưa được ghi trong doc comment của kiểu dữ liệu, thì chúng nên được ghi trong chú thích của từng method. Ví dụ:
package sql
// Close returns the connection to the connection pool.
// All operations after a Close will return with ErrConnDone.
// Close is safe to call concurrently with other operations and will
// block until all other operations finish. It may be useful to first
// cancel any used context and then call Close directly after.
func (c *Conn) Close() error {
...
}
Lưu ý rằng doc comment của hàm và method tập trung vào việc thao tác đó trả về gì hoặc làm gì, trình bày chi tiết những điều mà phía gọi cần biết. Các trường hợp đặc biệt có thể đặc biệt quan trọng để ghi lại trong tài liệu. Ví dụ:
package math
// Sqrt returns the square root of x.
//
// Special cases are:
//
// Sqrt(+Inf) = +Inf
// Sqrt(±0) = ±0
// Sqrt(x < 0) = NaN
// Sqrt(NaN) = NaN
func Sqrt(x float64) float64 {
...
}
Doc comment không nên giải thích các chi tiết nội bộ chẳng hạn như thuật toán đang được dùng trong bản hiện thực hiện tại. Những điều đó tốt nhất nên để cho các chú thích bên trong thân hàm. Có thể thích hợp khi nêu ra cận tiệm cận về thời gian hoặc bộ nhớ nếu chi tiết đó đặc biệt quan trọng với phía gọi. Ví dụ:
package sort
// Sort sorts data in ascending order as determined by the Less method.
// It makes one call to data.Len to determine n and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
...
}
Vì doc comment này không hề nhắc tới thuật toán sắp xếp nào đang được dùng, nên sau này việc đổi bản hiện thực sang thuật toán khác sẽ dễ dàng hơn.
Hằng số
Cú pháp khai báo của Go cho phép gom nhóm các khai báo, khi đó một doc comment duy nhất có thể giới thiệu cho cả một nhóm hằng số có liên quan, còn từng hằng số riêng lẻ chỉ được ghi chú bằng các chú thích ngắn ở cuối dòng. Ví dụ:
package scanner // import "text/scanner"
// The result of Scan is one of these tokens or a Unicode character.
const (
EOF = -(iota + 1)
Ident
Int
Float
Char
...
)
Đôi khi cả nhóm không cần doc comment nào cả. Ví dụ:
package unicode // import "unicode"
const (
MaxRune = '\U0010FFFF' // maximum valid Unicode code point.
ReplacementChar = '\uFFFD' // represents invalid code points.
MaxASCII = '\u007F' // maximum ASCII value.
MaxLatin1 = '\u00FF' // maximum Latin-1 value.
)
Mặt khác, các hằng số không nằm trong nhóm thường xứng đáng có một doc comment đầy đủ bắt đầu bằng một câu hoàn chỉnh. Ví dụ:
package unicode
// Version is the Unicode edition from which the tables are derived.
const Version = "13.0.0"
Các hằng số có kiểu dữ liệu được hiển thị ngay cạnh phần khai báo kiểu dữ liệu của chúng, và do đó thường bỏ qua doc comment cho nhóm const, nhường chỗ cho doc comment của kiểu dữ liệu. Ví dụ:
package syntax
// An Op is a single regular expression operator.
type Op uint8
const (
OpNoMatch Op = 1 + iota // matches no strings
OpEmptyMatch // matches empty string
OpLiteral // matches Runes sequence
OpCharClass // matches Runes interpreted as range pair list
OpAnyCharNotNL // matches any character except newline
...
)
(Xem pkg.go.dev/regexp/syntax#Op để thấy bản trình bày HTML.)
Biến
Quy ước cho biến cũng giống như quy ước cho hằng số. Ví dụ, đây là một nhóm biến:
package fs
// Generic file system errors.
// Errors returned by file systems can be tested against these errors
// using errors.Is.
var (
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
)
Và một biến đơn lẻ:
package unicode
// Scripts is the set of Unicode script tables.
var Scripts = map[string]*RangeTable{
"Adlam": Adlam,
"Ahom": Ahom,
"Anatolian_Hieroglyphs": Anatolian_Hieroglyphs,
"Arabic": Arabic,
"Armenian": Armenian,
...
}
Cú pháp
Doc comment trong Go được viết bằng một cú pháp đơn giản hỗ trợ đoạn văn, heading, link, danh sách và khối code định dạng sẵn. Để chú thích luôn nhẹ nhàng và dễ đọc trong tệp mã nguồn, cú pháp này không hỗ trợ các tính năng phức tạp như đổi font hay HTML thô. Người mê Markdown có thể xem cú pháp này như một tập con đơn giản hoá của Markdown.
Bộ định dạng chuẩn gofmt định dạng lại doc comment để dùng một cách trình bày chuẩn tắc cho từng tính năng nói trên. Gofmt hướng tới tính dễ đọc và quyền kiểm soát của người dùng đối với cách viết chú thích trong mã nguồn, nhưng sẽ điều chỉnh phần trình bày để làm cho ý nghĩa ngữ nghĩa của một chú thích cụ thể trở nên rõ ràng hơn, tương tự như việc định dạng lại 1+2 * 3 thành 1 + 2*3 trong mã nguồn thông thường.
Gofmt xoá các dòng trống ở đầu và cuối doc comment. Nếu mọi dòng trong một doc comment đều bắt đầu bằng cùng một chuỗi khoảng trắng và tab, gofmt sẽ xoá tiền tố đó.
Đoạn văn
Một đoạn văn là một dãy các dòng không trống và không thụt lề. Chúng ta đã thấy rất nhiều ví dụ về đoạn văn.
Một cặp dấu backtick liên tiếp (` U+0060) được hiểu là dấu nháy trái Unicode (“ U+201C), còn một cặp dấu nháy đơn liên tiếp (’ U+0027) được hiểu là dấu nháy phải Unicode (” U+201D).
Gofmt giữ nguyên các chỗ xuống dòng trong văn bản đoạn văn: nó không ngắt dòng lại. Điều này cho phép dùng semantic linefeeds, như đã thấy ở trên. Gofmt thay các dòng trống bị lặp giữa các đoạn văn bằng một dòng trống duy nhất. Gofmt cũng định dạng lại các dấu backtick hoặc dấu nháy đơn liên tiếp thành cách hiểu Unicode của chúng.
Ghi chú
Ghi chú là những chú thích đặc biệt có dạng MARKER(uid): body. MARKER nên gồm từ 2 chữ cái viết hoa [A-Z] trở lên, xác định loại ghi chú, còn uid có ít nhất 1 ký tự, thường là tên người dùng của ai đó có thể cung cấp thêm thông tin. Dấu : theo sau uid là tuỳ chọn.
Các ghi chú được thu thập và hiển thị trong một mục riêng trên pkg.go.dev.
Ví dụ:
// TODO(user1): refactor to use standard library context
// BUG(user2): not cleaned up
var ctx context.Context
Thông báo ngừng hỗ trợ
Các đoạn văn bắt đầu bằng Deprecated: được xem là thông báo deprecation. Một số công cụ sẽ cảnh báo khi các identifier bị deprecated được sử dụng. pkg.go.dev sẽ ẩn tài liệu của chúng theo mặc định.
Thông báo deprecation được theo sau bởi một số thông tin về việc deprecation đó, và khuyến nghị về thứ nên dùng thay thế, nếu có. Đoạn văn này không nhất thiết phải là đoạn cuối cùng trong doc comment.
Ví dụ:
// Package rc4 implements the RC4 stream cipher.
//
// Deprecated: RC4 is cryptographically broken and should not be used
// except for compatibility with legacy systems.
//
// This package is frozen and no new functionality will be added.
package rc4
// Reset zeros the key data and makes the Cipher unusable.
//
// Deprecated: Reset can't guarantee that the key will be entirely removed from
// the process's memory.
func (c *Cipher) Reset()
Heading
Một heading là một dòng bắt đầu bằng dấu thăng (U+0023) rồi tới một khoảng trắng và nội dung tiêu đề. Để được nhận diện là heading, dòng đó phải không thụt lề và được tách khỏi văn bản đoạn văn liền kề bằng các dòng trống.
Ví dụ:
// Package strconv implements conversions to and from string representations
// of basic data types.
//
// # Numeric Conversions
//
// The most common numeric conversions are [Atoi] (string to int) and [Itoa] (int to string).
...
package strconv
Mặt khác:
// #This is not a heading, because there is no space.
//
// # This is not a heading,
// # because it is multiple lines.
//
// # This is not a heading,
// because it is also multiple lines.
//
// The next paragraph is not a heading, because there is no additional text:
//
// #
//
// In the middle of a span of non-blank lines,
// # this is not a heading either.
//
// # This is not a heading, because it is indented.
Cú pháp # được thêm vào từ Go 1.19. Trước Go 1.19, heading được nhận diện ngầm định qua các đoạn văn một dòng thoả mãn một số điều kiện nhất định, đáng chú ý nhất là không có dấu câu kết thúc.
Gofmt định dạng lại những dòng bị các phiên bản Go trước đó xem là heading ngầm định để dùng heading dạng # thay thế. Nếu việc định dạng lại đó không phù hợp — tức là nếu dòng đó vốn không có ý là một heading — thì cách dễ nhất để biến nó thành đoạn văn là thêm dấu câu kết thúc như dấu chấm hoặc dấu hai chấm, hoặc tách nó thành hai dòng.
Link
Một dãy các dòng không trống và không thụt lề sẽ định nghĩa các đích của link khi mọi dòng đều có dạng “[Text]: URL”. Trong phần văn bản khác của cùng doc comment đó, “[Text]” biểu diễn một link tới URL với nội dung hiển thị đã cho — trong HTML là <a href=“URL”>Text</a>. Ví dụ:
// Package json implements encoding and decoding of JSON as defined in
// [RFC 7159]. The mapping between JSON and Go values is described
// in the documentation for the Marshal and Unmarshal functions.
//
// For an introduction to this package, see the article
// “[JSON and Go].”
//
// [RFC 7159]: https://tools.ietf.org/html/rfc7159
// [JSON and Go]: https://golang.org/doc/articles/json_and_go.html
package json
Bằng cách giữ các URL trong một mục riêng, định dạng này chỉ làm gián đoạn dòng chảy của văn bản thực sự ở mức tối thiểu. Nó cũng gần khớp với shortcut reference link format của Markdown, chỉ thiếu phần tiêu đề tuỳ chọn.
Nếu không có khai báo URL tương ứng, thì (ngoại trừ doc link, được mô tả ở mục kế tiếp) “[Text]” không phải là một siêu liên kết, và các dấu ngoặc vuông được giữ nguyên khi hiển thị. Mỗi doc comment được xét một cách độc lập: định nghĩa đích link trong một chú thích không ảnh hưởng tới các chú thích khác.
Mặc dù các khối định nghĩa đích link có thể xen kẽ với các đoạn văn thông thường, gofmt sẽ dời mọi định nghĩa đích link xuống cuối doc comment, thành tối đa hai khối: trước hết là khối chứa mọi đích link có được tham chiếu trong chú thích, rồi tới khối chứa mọi đích không được tham chiếu trong chú thích. Khối riêng này giúp dễ nhận ra và sửa các đích không dùng tới (phòng khi link hoặc định nghĩa có lỗi chính tả) hoặc xoá chúng đi (phòng khi các định nghĩa không còn cần nữa).
Văn bản thuần được nhận diện là URL sẽ tự động được biến thành link trong bản kết xuất HTML.
Doc link
Doc link là các link có dạng “[Name1]” hoặc “[Name1.Name2]” để trỏ tới các identifier được export trong package hiện tại, hoặc “[pkg]”, “[pkg.Name1]”, hay “[pkg.Name1.Name2]” để trỏ tới identifier trong các package khác.
Ví dụ:
package bytes
// ReadFrom reads data from r until EOF and appends it to the buffer, growing
// the buffer as needed. The return value n is the number of bytes read. Any
// error except [io.EOF] encountered during the read is also returned. If the
// buffer becomes too large, ReadFrom will panic with [ErrTooLarge].
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
...
}
Văn bản trong ngoặc vuông cho một link tới symbol có thể chứa một dấu sao ở đầu (tuỳ chọn), giúp dễ dàng trỏ tới các kiểu con trỏ, chẳng hạn [*bytes.Buffer].
Khi trỏ tới package khác, “pkg” có thể là một import path đầy đủ hoặc tên package được giả định của một import đang có. Tên package được giả định là identifier trong một import được đổi tên, hoặc tên được goimports giả định. (Goimports chèn thêm phần đổi tên khi giả định đó không đúng, nên quy tắc này về cơ bản sẽ hoạt động với hầu như mọi mã Go.) Ví dụ, nếu package hiện tại import encoding/json, thì “[json.Decoder]” có thể được viết thay cho “[encoding/json.Decoder]” để liên kết tới tài liệu của Decoder trong encoding/json. Nếu các tệp mã nguồn khác nhau trong cùng một package import các package khác nhau dưới cùng một tên, thì dạng viết tắt sẽ nhập nhằng và không thể dùng được.
Một “pkg” chỉ được giả định là import path đầy đủ nếu nó bắt đầu bằng một tên miền (một thành phần đường dẫn có chứa dấu chấm) hoặc là một trong các package thuộc thư viện chuẩn (“[os]”, “[encoding/json]”, và tương tự). Ví dụ, [os.File] và [example.com/sys.File] là các documentation link (link thứ hai sẽ là link hỏng), nhưng [os/sys.File] thì không, vì không có package os/sys nào trong thư viện chuẩn.
Để tránh rắc rối với map, generics và kiểu mảng, doc link phải được đặt trước và sau bởi dấu câu, khoảng trắng, tab, hoặc điểm bắt đầu hay kết thúc của một dòng. Ví dụ, văn bản “map[ast.Expr]TypeAndValue” không chứa doc link nào.
Danh sách
Một danh sách là một dãy các dòng thụt lề hoặc dòng trống (mà nếu không thì sẽ là một khối code, như mô tả ở mục kế tiếp) trong đó dòng thụt lề đầu tiên bắt đầu bằng một dấu đầu dòng hoặc một dấu đánh số.
Dấu đầu dòng là dấu sao, dấu cộng, dấu gạch ngang, hoặc dấu chấm tròn Unicode (*, +, -, •; U+002A, U+002B, U+002D, U+2022) theo sau là một khoảng trắng hoặc tab rồi tới văn bản. Trong một danh sách gạch đầu dòng, mỗi dòng bắt đầu bằng một dấu đầu dòng sẽ mở đầu một mục mới.
Ví dụ:
package url
// PublicSuffixList provides the public suffix of a domain. For example:
// - the public suffix of "example.com" is "com",
// - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and
// - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us".
//
// Implementations of PublicSuffixList must be safe for concurrent use by
// multiple goroutines.
//
// An implementation that always returns "" is valid and may be useful for
// testing but it is not secure: it means that the HTTP server for foo.com can
// set a cookie for bar.com.
//
// A public suffix list implementation is in the package
// golang.org/x/net/publicsuffix.
type PublicSuffixList interface {
...
}
Dấu đánh số là một số thập phân có độ dài tuỳ ý theo sau là dấu chấm hoặc dấu ngoặc đơn phải, rồi một khoảng trắng hoặc tab, rồi tới văn bản. Trong một danh sách đánh số, mỗi dòng bắt đầu bằng một dấu đánh số sẽ mở đầu một mục mới. Số thứ tự của các mục được giữ nguyên, không bao giờ bị đánh số lại.
Ví dụ:
package path
// Clean returns the shortest path name equivalent to path
// by purely lexical processing. It applies the following rules
// iteratively until no further processing can be done:
//
// 1. Replace multiple slashes with a single slash.
// 2. Eliminate each . path name element (the current directory).
// 3. Eliminate each inner .. path name element (the parent directory)
// along with the non-.. element that precedes it.
// 4. Eliminate .. elements that begin a rooted path:
// that is, replace "/.." by "/" at the beginning of a path.
//
// The returned path ends in a slash only if it is the root "/".
//
// If the result of this process is an empty string, Clean
// returns the string ".".
//
// See also Rob Pike, “[Lexical File Names in Plan 9].”
//
// [Lexical File Names in Plan 9]: https://9p.io/sys/doc/lexnames.html
func Clean(path string) string {
...
}
Các mục trong danh sách chỉ chứa đoạn văn, không chứa khối code hay danh sách lồng nhau. Điều này tránh mọi sự tinh vi trong việc đếm khoảng trắng cũng như các câu hỏi về việc một tab được tính bằng bao nhiêu khoảng trắng khi thụt lề không nhất quán.
Gofmt định dạng lại danh sách gạch đầu dòng để dùng dấu gạch ngang làm dấu đầu dòng, thụt lề hai khoảng trắng trước dấu gạch ngang, và thụt lề bốn khoảng trắng cho các dòng tiếp nối.
Gofmt định dạng lại danh sách đánh số để dùng một khoảng trắng trước con số, một dấu chấm sau con số, và cũng thụt lề bốn khoảng trắng cho các dòng tiếp nối.
Gofmt giữ nguyên nhưng không đòi hỏi phải có một dòng trống giữa danh sách và đoạn văn phía trước. Nó chèn một dòng trống giữa danh sách và đoạn văn hoặc heading phía sau.
Khối code
Một khối code là một dãy các dòng thụt lề hoặc dòng trống không bắt đầu bằng dấu đầu dòng hay dấu đánh số. Nó được kết xuất dưới dạng văn bản định dạng sẵn (một khối <pre> trong HTML).
Khối code thường chứa mã Go. Ví dụ:
package sort
// Search uses binary search...
//
// As a more whimsical example, this program guesses your number:
//
// func GuessingGame() {
// var s string
// fmt.Printf("Pick an integer from 0 to 100.\n")
// answer := sort.Search(100, func(i int) bool {
// fmt.Printf("Is your number <= %d? ", i)
// fmt.Scanf("%s", &s)
// return s != "" && s[0] == 'y'
// })
// fmt.Printf("Your number is %d.\n", answer)
// }
func Search(n int, f func(int) bool) int {
...
}
Tất nhiên, khối code cũng thường chứa văn bản định dạng sẵn không phải là mã. Ví dụ:
package path
// Match reports whether name matches the shell pattern.
// The pattern syntax is:
//
// pattern:
// { term }
// term:
// '*' matches any sequence of non-/ characters
// '?' matches any single non-/ character
// '[' [ '^' ] { character-range } ']'
// character class (must be non-empty)
// c matches character c (c != '*', '?', '\\', '[')
// '\\' c matches character c
//
// character-range:
// c matches character c (c != '\\', '-', ']')
// '\\' c matches character c
// lo '-' hi matches character c for lo <= c <= hi
//
// Match requires pattern to match all of name, not just a substring.
// The only possible returned error is [ErrBadPattern], when pattern
// is malformed.
func Match(pattern, name string) (matched bool, err error) {
...
}
Gofmt thụt lề mọi dòng trong một khối code bằng đúng một tab, thay cho bất kỳ phần thụt lề chung nào khác mà các dòng không trống đang có. Gofmt cũng chèn một dòng trống trước và sau mỗi khối code, phân biệt rõ khối code với phần văn bản đoạn văn xung quanh.
Chỉ thị
Các chú thích chỉ thị như //go:generate không được xem là một phần của doc comment và bị bỏ qua khi kết xuất tài liệu. Gofmt dời các chú thích chỉ thị xuống cuối doc comment, phía trước có một dòng trống. Ví dụ:
package regexp
// An Op is a single regular expression operator.
//
//go:generate stringer -type Op -trimprefix Op
type Op uint8
Chú thích chỉ thị là một dòng bắt đầu bằng biểu thức chính quy //(line |extern |export |[a-z0-9]+:[a-z0-9]).
Các công cụ có thể định nghĩa chú thích chỉ thị riêng của mình theo dạng //toolname:directive arguments. Chỉ thị của công cụ khớp với biểu thức chính quy //([a-z0-9]+):([a-z0-9]\PZ*)($|\pZ+)(.*), trong đó nhóm thứ nhất là tên công cụ và nhóm thứ hai là tên chỉ thị. Các đối số tuỳ chọn được tách khỏi tên chỉ thị bằng một hoặc nhiều ký tự khoảng trắng Unicode. Mỗi công cụ có thể định nghĩa cú pháp đối số riêng, nhưng một quy ước phổ biến là một dãy các đối số cách nhau bằng khoảng trắng, trong đó một đối số có thể là một từ trần, hoặc một chuỗi Go đặt trong nháy kép hay nháy ngược. Tên công cụ go được dành riêng cho toolchain của Go.
Hàm go/ast.ParseDirective và các kiểu dữ liệu liên quan của nó phân tích cú pháp chỉ thị của công cụ.
Những lỗi và cạm bẫy thường gặp
Quy tắc rằng bất kỳ dãy dòng thụt lề hoặc dòng trống nào trong doc comment đều được kết xuất thành khối code đã có từ những ngày đầu tiên của Go. Đáng tiếc, việc gofmt không hỗ trợ doc comment đã dẫn tới nhiều chú thích hiện có dùng thụt lề mà không hề có ý tạo ra một khối code.
Ví dụ, danh sách không thụt lề dưới đây từ trước tới nay luôn được godoc hiểu là một đoạn văn ba dòng theo sau bởi một khối code một dòng:
package http
// cancelTimerBody is an io.ReadCloser that wraps rc with two features:
// 1) On Read error or close, the stop func is called.
// 2) On Read failure, if reqDidTimeout is true, the error is wrapped and
// marked as net.Error that hit its timeout.
type cancelTimerBody struct {
...
}
Nó luôn được go doc kết xuất thành:
cancelTimerBody is an io.ReadCloser that wraps rc with two features:
1) On Read error or close, the stop func is called. 2) On Read failure,
if reqDidTimeout is true, the error is wrapped and
marked as net.Error that hit its timeout.
Tương tự, câu lệnh trong chú thích này là một đoạn văn một dòng theo sau bởi một khối code một dòng:
package smtp
// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
//
// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var localhostCert = []byte(`...`)
Nó được go doc kết xuất thành:
localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
--ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
Và chú thích này là một đoạn văn hai dòng (dòng thứ hai là “{”), theo sau bởi một khối code thụt lề sáu dòng và một đoạn văn một dòng (“}”).
// On the wire, the JSON will look something like this:
// {
// "kind":"MyAPIObject",
// "apiVersion":"v1",
// "myPlugin": {
// "kind":"PluginA",
// "aOption":"foo",
// },
// }
Và nó được go doc kết xuất thành:
On the wire, the JSON will look something like this: {
"kind":"MyAPIObject",
"apiVersion":"v1",
"myPlugin": {
"kind":"PluginA",
"aOption":"foo",
},
}
Một lỗi thường gặp khác là định nghĩa hàm Go hoặc khối câu lệnh không thụt lề, cũng được bao bởi “{” và “}” tương tự như vậy.
Việc gofmt của Go 1.19 bắt đầu định dạng lại doc comment khiến những lỗi như thế này dễ thấy hơn, nhờ việc thêm các dòng trống quanh khối code.
Một phân tích vào năm 2022 cho thấy chỉ 3% doc comment trong các module Go công khai bị bản gofmt nháp của Go 1.19 định dạng lại. Chỉ xét riêng những chú thích đó, khoảng 87% số lần gofmt định dạng lại vẫn giữ được cấu trúc mà một người sẽ suy ra khi đọc chú thích; khoảng 6% bị vấp bởi chính những kiểu danh sách không thụt lề, câu lệnh shell nhiều dòng không thụt lề, và khối code bao bởi ngoặc nhọn không thụt lề.
Dựa trên phân tích này, gofmt của Go 1.19 áp dụng một vài phỏng đoán để gộp các dòng không thụt lề vào danh sách hoặc khối code thụt lề liền kề. Với những điều chỉnh đó, gofmt của Go 1.19 định dạng lại các ví dụ trên thành:
// cancelTimerBody is an io.ReadCloser that wraps rc with two features:
// 1. On Read error or close, the stop func is called.
// 2. On Read failure, if reqDidTimeout is true, the error is wrapped and
// marked as net.Error that hit its timeout.
// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
//
// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
// On the wire, the JSON will look something like this:
//
// {
// "kind":"MyAPIObject",
// "apiVersion":"v1",
// "myPlugin": {
// "kind":"PluginA",
// "aOption":"foo",
// },
// }
Việc định dạng lại này làm cho ý nghĩa rõ ràng hơn, đồng thời khiến doc comment được kết xuất đúng trên cả những phiên bản Go cũ hơn. Nếu phỏng đoán đó có lúc nào ra quyết định sai, bạn có thể ghi đè nó bằng cách chèn một dòng trống để tách bạch rõ ràng phần văn bản đoạn văn với phần không phải đoạn văn.
Ngay cả khi có những phỏng đoán này, các chú thích hiện có khác vẫn sẽ cần chỉnh sửa thủ công để kết xuất đúng. Lỗi phổ biến nhất là thụt lề một dòng văn bản tiếp nối vốn không thụt lề. Ví dụ:
// TODO Revisit this design. It may make sense to walk those nodes
// only once.
// According to the document:
// "The alignment factor (in bytes) that is used to align the raw data of sections in
// the image file. The value should be a power of 2 between 512 and 64 K, inclusive."
Ở cả hai trường hợp, dòng cuối cùng bị thụt lề, biến nó thành một khối code. Cách sửa là bỏ thụt lề ở những dòng đó.
Một lỗi phổ biến khác là không thụt lề dòng tiếp nối của một danh sách hoặc khối code vốn đã thụt lề. Ví dụ:
// Uses of this error model include:
//
// - Partial errors. If a service needs to return partial errors to the
// client,
// it may embed the `Status` in the normal response to indicate the
// partial
// errors.
//
// - Workflow errors. A typical workflow has multiple steps. Each step
// may
// have a `Status` message for error reporting.
Cách sửa là thụt lề các dòng tiếp nối.
Doc comment trong Go không hỗ trợ danh sách lồng nhau, nên gofmt định dạng lại
// Here is a list:
//
// - Item 1.
// * Subitem 1.
// * Subitem 2.
// - Item 2.
// - Item 3.
thành
// Here is a list:
//
// - Item 1.
// - Subitem 1.
// - Subitem 2.
// - Item 2.
// - Item 3.
Viết lại văn bản để tránh danh sách lồng nhau thường sẽ cải thiện tài liệu và là giải pháp tốt nhất. Một cách khắc phục khả dĩ khác là trộn lẫn các loại dấu danh sách, vì dấu đầu dòng không mở đầu mục mới trong một danh sách đánh số, và ngược lại. Ví dụ:
// Here is a list:
//
// 1. Item 1.
//
// - Subitem 1.
//
// - Subitem 2.
//
// 2. Item 2.
//
// 3. Item 3.