Not Dự Blog

Golang Style Guide

Một vài quy ước khi code golang giúp code của mình dễ đọc, quản lý và nâng cấp hơn.
Published Jan 22, 2022

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:

  1. 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.
  2. 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

BadGood
type Handler struct {
  // ...
}
func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  ...
}
type Handler struct {
  // ...
}
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

Đ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.Mutexsync.RWMutex là hợp lệ, bạn không cần dùng con trỏ tới mutex.

BadGood
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()
Nếu bạn dùng con trỏ tới struct, con trỏ mutex phải là một trường non-pointer ở trong đó. Đừng gán mutex vào struct dù đó là unexported struct.
BadGood
type SMap struct {
  sync.Mutex
  data map[string]string
}
func NewSMap() *SMap {
  return &SMap{
    data: make(map[string]string),
  }
}
func (m *SMap) Get(k string) string {
  m.Lock()
  defer m.Unlock()
  return m.data[k]
}
type SMap struct {
  mu sync.Mutex
  data map[string]string
}
func NewSMap() *SMap {
  return &SMap{
    data: make(map[string]string),
  }
}
func (m *SMap) Get(k string) string {
  m.mu.Lock()
  defer m.mu.Unlock()
  return m.data[k]
}

The Mutex field, and the Lock and Unlock methods are unintentionally part of the exported API of SMap.

The mutex and its methods are implementation details of SMap hidden from its callers.

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
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// Did you mean to modify d1.trips?
trips[0] = ...
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}
trips := ...
d1.SetTrips(trips)
// We can now modify trips[0] without affecting d1.trips.
trips[0] = ...

Tương tự, bạn cũng cần phải cẩn thận khi trả về slice hoặc map.

BadGood
type Stats struct {
  mu sync.Mutex
  counters map[string]int
}
// Snapshot returns the current stats.
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()
  return s.counters
}
// snapshot is no longer protected by the mutex, so any
// access to the snapshot is subject to data races.
snapshot := stats.Snapshot()
type Stats struct {
  mu sync.Mutex
  counters map[string]int
}
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()
  result := make(map[string]int, len(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}
// Snapshot is now a copy.
snapshot := stats.Snapshot()

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.

BadGood
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// easy to miss unlocks due to multiple returns
p.Lock()
defer p.Unlock()
if p.count < 10 {
  return p.count
}
p.count++
return p.count
// more readable

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.

BadGood
type Operation int
const (
  Add Operation = iota
  Subtract
  Multiply
)
// Add=0, Subtract=1, Multiply=2
type Operation int
const (
  Add Operation = iota + 1
  Subtract
  Multiply
)
// Add=1, Subtract=2, Multiply=3

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ặc errors.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ùng fmt.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 matchingError matching
// package foo
func Open() error {
  return errors.New("could not open")
}
// package bar
if err := foo.Open(); err != nil {
  // Can't handle the error.
  panic("unknown error")
}
// package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
  return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrCouldNotOpen) {
    // handle the error
  } else {
    panic("unknown error")
  }
}

Đố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 matchingError matching
// package foo
func Open(file string) error {
  return fmt.Errorf("file %q not found", file)
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
  // Can't handle the error.
  panic("unknown error")
}
// package foo
type NotFoundError struct {
  File string
}
func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}
func Open(file string) error {
  return &NotFoundError{File: file}
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
  var notFound *NotFoundError
  if errors.As(err, &notFound) {
    // handle the error
  } else {
    panic("unknown error")
  }
}

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%w
  • Thêm thông tin, context với fmt.Errorf%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ới errors.Iserrors.As.
  • Dùng %v sẽ không thể bắt được các error với các hàm errors.Iserrors.As.

Khi thêm thông tin vào error, hạn chế dùng cụm từ “failed to”, ví dụ:

BadGood
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %w", err)
}
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %w", err)
}
failed to x: failed to y: failed to create new store: the error
x: y: new store: the error
See also [Don't just check errors, handle them gracefully].

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…

References

  1. Effective Go
  2. Go Common Mistakes
  3. Go Code Review Comments
  4. Uber Go Style Guide