Có một số quy ước và lỗi thường gặp mà nếu mình biết ngay từ đầu khi học golang thì có lẽ sẽ dễ thở hơn cho mình ở đoạn sau. Dưới đây là một số code conventions mà mình học được hoặc là đã gặp qua ở đâu đó trong lúc mình code.
Pointers to Interfaces
Go interface khá là đặc biệt so với interface của những ngôn ngữ khác, bạn không cần phải khai báo type của bạn implement interface, chỉ cần có đủ các method đã được định nghĩa trong interface, compiler sẽ lo phần còn lại.
Ex: https://go.dev/play/p/erodX-JplO
Một interface của go gồm 2 phần:
- Con trỏ trỏ tới thông tin của một kiểu dữ liệu cụ thể nào đó. Có thể gọi nó là type.
- Con trỏ tới data từ giá trị mà bạn truyền vào interface
data := c
w := Walker{
type: &InterfaceType{
valtype: &typeof(c),
func0: &Camel.Walk
}
data: &data
}
Thường bạn không cần con trỏ tới một interface trừ khi bạn muốn các method của interface thay đổi dữ liệu phía dưới.
Verify Interface Compliance
Bad | Good |
---|---|
|
|
Đoạn var _ http.Handler = (*Handler)(nil)
sẽ fail lúc compile nếu Handler
không khớp với http.Handler
interface.
Phía bên phải của phép gán phải là zero value
của kiểu dữ liệu, nil
nếu đó là kiểu pointer (*Handler
), slices, maps và empty struct nếu là struct type.
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
See also: [Pointers vs. Values]: https://golang.org/doc/effective_go.html#pointers_vs_values
Zero-value Mutexes are Valid
Zero-value của sync.Mutex
và sync.RWMutex
là hợp lệ, bạn không cần dùng con trỏ tới mutex.
Bad | Good |
---|---|
|
|
Bad | Good |
---|---|
|
|
The |
The mutex and its methods are implementation details of |
Slice và map
Slice và map chứa con trỏ tới dữ liệu bên dưới của chúng nên chúng ta cần phải cẩn thận khi sử dụng 2 kiểu dữ liệu này. Slice và map mà bạn nhận được dưới dạng đối số có thể bị thay đổi nếu bạn lưu một tham chiếu tới nó.
Bad | Good |
---|---|
|
|
Tương tự, bạn cũng cần phải cẩn thận khi trả về slice hoặc map.
Bad | Good |
---|---|
|
|
Defer trong go
Defer cho phép câu lệnh được gọi ra nhưng không thực hiện ngay mà hoãn lại cho đến khi những câu lệnh xung quanh trả về kết quả. Câu lệnh được gọi qua defer sẽ được đưa vào stack (LIFO). Defer thường được dùng để dọn dẹp các tài nguyên như file và lock hoặc đóng các kết nối khi chương trình kết thúc.
Bad | Good |
---|---|
|
|
Bắt đầu Enums với 1
Bạn nên bắt đầu enum với 1, trừ khi biến của bạn có giá trị mặc định là 0.
Bad | Good |
---|---|
|
|
Ví dụ trường hợp enum bắt đầu từ 0:
type OS int
const (
Unknown OS = iota
Android
IOS
)
// Unknown=0, Android=1, IOS=2
Errors
Tùy vào mục đích và tình huống sử dụng, bạn nên cân nhắc các kiểu error và dùng cho phù hợp:
- Nếu bạn cần xử lý một error cụ thể nào đó, chúng ta cần phải khai báo một top-level error hoặc một custom type error và kết hợp với các hàm
errors.Is
hoặcerrors.As
. - Nếu error message là một static string, bạn có thể dùng
errors.New
, còn nếu là dynamic string thì dùngfmt.Errorf
hoặc một custom error.
Error matching? | Error Message | Guidance |
---|---|---|
No | static | errors.New |
No | dynamic | fmt.Errorf |
Yes | static | top-level var with errors.New |
Yes | dynamic | custom error type |
Ví dụ, tạo một error với errors.New
và static string, sau đó export error này như một biến và dùng errors.Is
để bắt nó và xử lý.
No error matching | Error matching |
---|---|
|
|
Đối với dynamic string error, dùng fmt.Errorf
nếu không cần phải xử lý error đó, ngược lại, dùng một custom error.
No error matching | Error matching |
---|---|
|
|
Có 3 cách để truyền error nếu hàm gọi bị lỗi:
- Trả về error gốc
- Thêm thông tin, context với
fmt.Errorf
và%w
- Thêm thông tin, context với
fmt.Errorf
và%v
Trả về error gốc khi bạn không thêm bất kỳ context nào, việc này sẽ giúp giữ nguyên type và message của error. Thích hợp cho các error có đầy đủ các thông tin cần thiết khi kiểm tra lỗi.
Nếu bạn muốn thêm các thông tin khác vào error (VD: thay vì nhận được một error với message mơ hồ “connection refused”, bạn sẽ nhận được “call service abc: connection refused”) thì hãy dùng fmt.Errorf
kết hợp với %w
hoặc %v
.
- Dùng
%w
để wrap error lại và sau đó có thể upwrap với errors.Unwrap, nhờ vậy mà chúng ta có thể xử lý error vớierrors.Is
vàerrors.As
. - Dùng
%v
sẽ không thể bắt được các error với các hàmerrors.Is
vàerrors.As
.
Khi thêm thông tin vào error, hạn chế dùng cụm từ “failed to”, ví dụ:
Bad | Good |
---|---|
|
|
|
|
Error Naming
Đối với các biến error global, sử dụng các prefix Err
hoặc err
(tùy theo bạn có muốn export nó hay không).
var (
// The following two errors are exported
// so that users of this package can match them
// with errors.Is.
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// This error is not exported because
// we don't want to make it part of our public API.
// We may still use it inside the package
// with errors.Is.
errNotFound = errors.New("not found")
)
Đối với các kiểu custom error, dùng suffix Error
.
// Similarly, this error is exported
// so that users of this package can match it
// with errors.As.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// And this error is not exported because
// we don't want to make it part of the public API.
// We can still use it inside the package
// with errors.As.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
To be continue…