Tránh Sử Dụng Dòng Quá Dài

Tránh sử dụng các dòng mã yêu cầu người đọc cuộn ngang hoặc làm văn bản quay quá mức.

Chúng tôi đề xuất giới hạn độ dài dòng mã tối đa là 99 ký tự. Tác giả nên chia dòng trước giới hạn này, nhưng đó không phải là một quy tắc cứng nhắc. Việc mã vượt quá giới hạn này cũng là chấp nhận được.

Tính Đồng Nhất

Một số tiêu chuẩn đề cập trong tài liệu này dựa trên nhận định chủ quan, tình huống hoặc ngữ cảnh. Tuy nhiên, phần quan trọng nhất là duy trì tính đồng nhất.

Mã đồng nhất dễ dàng bảo trì hơn, có tính hợp lý hơn, đòi hỏi ít chi phí học tập hơn và dễ dàng di chuyển, cập nhật và sửa lỗi khi các quy ước mới nổi lên hoặc lỗi xảy ra.

Ngược lại, việc bao gồm nhiều kiểu mã hoàn toàn khác nhau hoặc xung đột trong một dự án mã nguồn mở dẫn đến việc tăng chi phí bảo trì, sự không chắc chắn và thiên lệch nhận thức. Tất cả điều này dẫn đến tốc độ chậm, cuộc xem xét mã đau đớn và tăng số lượng lỗi trực tiếp.

Khi áp dụng các tiêu chuẩn này vào một dự án mã nguồn mở, khuyến nghị thực hiện các thay đổi ở mức gói (hoặc lớn hơn). Áp dụng nhiều kiểu mã tại mức phụ gói vi phạm những quan tâm trên.

Nhóm Các Khai Báo Tương Tự

Ngôn ngữ Go hỗ trợ nhóm các khai báo tương tự.

Không Khuyến Nghị:

import "a"
import "b"

Khuyến Nghị:

import (
  "a"
  "b"
)

Điều này cũng áp dụng cho các khai báo hằng, biến và kiểu:

Không Khuyến Nghị:

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

Khuyến Nghị:

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

Chỉ nhóm các khai báo liên quan với nhau và tránh nhóm các khai báo không liên quan.

Không Khuyến Nghị:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)

Khuyến Nghị:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

Không có hạn chế về nơi sử dụng nhóm. Ví dụ, bạn có thể sử dụng chúng trong một hàm:

Không Khuyến Nghị:

func f() string {
  red := color.New(0xff0000)
  green := color.New(0x00ff00)
  blue := color.New(0x0000ff)

  ...
}

Khuyến Nghị:

func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  ...
}

Ngoại lệ: Nếu các khai báo biến kế tiếp tới các biến khác, đặc biệt là trong các khai báo cục bộ hàm, chúng nên được nhóm lại với nhau. Thực hiện điều này ngay cả đối với các biến không liên quan được khai báo cùng nhau.

Không Khuyến Nghị:

func (c *client) request() {
  caller := c.name
  format := "json"
  timeout := 5*time.Second
  var err error
  // ...
}

Khuyến Nghị:

func (c *client) request() {
  var (
    caller  = c.name
    format  = "json"
    timeout = 5*time.Second
    err error
  )
  // ...
}

Nhóm Import

Các import nên được nhóm thành hai danh mục:

  • Thư viện chuẩn
  • Các thư viện khác

Mặc định, đây là cách nhóm áp dụng bởi goimports. Không Khuyến Nghị:

import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

Khuyến Nghị:

import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

Tên Gói

Khi đặt tên cho một gói, hãy tuân theo các quy tắc sau:

  • Viết thường tất cả, không viết hoa hoặc gạch dưới.
  • Trong hầu hết các trường hợp, không cần đổi tên khi nhập.
  • Ngắn gọn và súc tích. Hãy nhớ rằng tên đầy đủ ở mọi nơi sử dụng.
  • Tránh dùng số nhiều. Ví dụ, sử dụng net/url thay vì net/urls.
  • Tránh sử dụng "common," "util," "shared," hoặc "lib." Những từ này không cung cấp đủ thông tin.

Đặt tên hàm

Chúng tôi tuân theo quy ước của cộng đồng Go sử dụng MixedCaps cho tên hàm. Một ngoại lệ được tạo ra cho việc nhóm các trường hợp kiểm thử liên quan, trong đó tên hàm có thể chứa dấu gạch dưới, ví dụ: TestMyFunction_WhatIsBeingTested.

Bí danh nhập

Nếu tên gói không khớp với phần tử cuối cùng của đường dẫn nhập, bí danh nhập phải được sử dụng.

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

Trong tất cả các trường hợp khác, bí danh nhập nên tránh được sử dụng trừ khi có xung đột trực tiếp giữa các nhập.

Không nên:

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

Nên sử dụng:

import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

Nhóm và Thứ tự Hàm

  • Các hàm nên được sắp xếp xấp xỉ theo thứ tự chúng được gọi.
  • Các hàm trong cùng một tệp nên được nhóm theo bộ nhận. Vì vậy, các hàm được xuất nên xuất hiện đầu tiên trong tệp, đặt sau các định nghĩa struct, const, và var.

Một newXYZ()/NewXYZ() có thể xuất hiện sau các định nghĩa kiểu nhưng trước các phương thức còn lại của bộ nhận.

Khi các hàm được nhóm theo bộ nhận, các hàm tiện ích chung nên xuất hiện cuối cùng trong tệp. Không nên:

func (s *something) Cost() {
  return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n []int) int {...}

func (s *something) Stop() {...}

func newSomething() *something {
    return &something{}
}

Nên sử dụng:

type something struct{ ... }

func newSomething() *something {
    return &something{}
}

func (s *something) Cost() {
  return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []int) int {...}

Giảm sâu lồng

Mã nên giảm sâu lồng bằng cách xử lý trường hợp lỗi/trường hợp đặc biệt càng sớm càng tốt và hoặc trả về hoặc tiếp tục vòng lặp. Giảm sâu lồng giảm số lượng mã ở nhiều cấp độ.

Không nên:

for _, v := range data {
  if v.F1 == 1 {
    v = process(v)
    if err := v.Call(); err == nil {
      v.Send()
    } else {
      return err
    }
  } else {
    log.Printf("Invalid v: %v", v)
  }
}

Nên sử dụng:

for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

Không cần thiết else

Nếu một biến được thiết lập trong cả hai nhánh của một điều kiện if, nó có thể được thay thế bằng một câu lệnh if duy nhất.

Không nên:

var a int
if b {
  a = 100
} else {
  a = 10
}

Nên sử dụng:

a := 10
if b {
  a = 100
}

Khai báo Biến ở Mức độ Cao nhất

Ở mức độ cao nhất, sử dụng từ khóa var chuẩn. Không chỉ định kiểu trừ khi nó khác với kiểu của biểu thức.

Không nên:

var _s string = F()

func F() string { return "A" }

Nên sử dụng:

var _s = F()
// Vì F trả về rõ ràng kiểu chuỗi, chúng ta không cần chỉ định rõ ràng kiểu cho _s

func F() string { return "A" }

Chỉ định kiểu nếu nó không khớp chính xác với kiểu cần thiết cho biểu thức.

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F trả về một thể hiện của kiểu myError, nhưng chúng ta cần kiểu error

Sử dụng '_' làm Tiếp Đầu Ngữ cho Biến Toàn Cục và Hằng số Không Được Xuất

Đối với các biến toàn cục không được xuất và hằng số, đặt tiếp đầu ngữ với dấu gạch dưới _ để chỉ rõ tính toàn cầu của chúng khi sử dụng.

Lý do cơ bản: Biến và hằng số toàn cục có phạm vi gói. Sử dụng tên chung có thể dẫn đến việc sử dụng giá trị sai lầm trong các tệp khác.

Không được khuyến nghị:

// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Cổng mặc định", defaultPort)

  // Chúng ta sẽ không thấy lỗi biên dịch nếu dòng đầu tiên của
  // Bar() bị xóa.
}

Khuyến nghị:

// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

Ngoại lệ: Giá trị lỗi không được xuất có thể sử dụng tiếp đầu ngữ err mà không có dấu gạch dưới. Xem cách đặt tên của lỗi.

Nhúng trong Cấu Trúc

Các loại nhúng (như mutex) nên được đặt ở đầu danh sách trường trong cấu trúc và phải có một dòng trống ngăn cách các trường được nhúng với các trường thông thường.

Không được khuyến nghị:

type Client struct {
  version int
  http.Client
}

Khuyến nghị:

type Client struct {
  http.Client

  version int
}

Việc nhúng nên cung cấp lợi ích rõ ràng, chẳng hạn như thêm hoặc cải thiện chức năng theo cách ngữ nghĩa thích hợp. Nên sử dụng mà không ảnh hưởng tiêu cực đến người dùng. (Xem thêm: Tránh nhúng các loại trong các cấu trúc công cộng)

Ngoại lệ: Ngay cả trong các loại không được xuất, Mutex không nên được sử dụng làm trường nhúng. Xem thêm: Giá trị zero của Mutex là hợp lệ.

Nhúng không nên:

  • Tồn tại chỉ vì tính thẩm mỹ hoặc tiện lợi.
  • Làm cho việc xây dựng hoặc sử dụng loại ngoài trở nên khó khăn hơn.
  • Ảnh hưởng đến giá trị zero của loại ngoài. Nếu loại ngoài có một giá trị zero hữu ích, vẫn nên có một giá trị zero hữu ích sau khi nhúng loại bên trong.
  • Có hiệu ứng phụ là tiết lộ các chức năng hoặc trường không liên quan từ loại bên trong đã nhúng.
  • Tiết lộ các loại không được xuất.
  • Ảnh hưởng đến hình thức sao chép của loại ngoài.
  • Thay đổi API hoặc ngữ nghĩa loại của loại ngoài.
  • Nhúng loại bên trong theo một hình thức không chuẩn.
  • Tiết lộ chi tiết cài đặt của loại ngoài.
  • Cho phép người dùng quan sát hoặc kiểm soát loại nội bộ.
  • Thay đổi hành vi chung của các hàm nội bộ một cách đáng ngạc nhiên đối với người dùng.

Nói cách khác, hãy nhúng một cách tỉnh táo và mục đích. Một bài kiểm tra tốt là, "Tất cả các phương thức/trường được xuất này từ loại bên trong sẽ được thêm trực tiếp vào loại ngoài không?" Nếu câu trả lời là một ít hoặc không, đừng nhúng loại bên trong - hãy sử dụng các trường thay vào đó.

Không được khuyến nghị:

type A struct {
    // Xấu: A.Lock() và A.Unlock() bây giờ có sẵn
    // Không cung cấp lợi ích chức năng và cho phép người dùng kiểm soát chi tiết nội bộ của A.
    sync.Mutex
}

Khuyến nghị:

type countingWriteCloser struct {
    // Tốt: Trình Write() được cung cấp ở mức độ bên ngoài cho 1 mục đích cụ thể, 
    // và ủy quyền công việc cho hàm Write() của loại bên trong.
    io.WriteCloser
    count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}

Khai Báo Biến Cục Bộ

Nếu một biến được thiết lập một cách rõ ràng, nên sử dụng dạng khai báo biến ngắn (:=).

Không được khuyến nghị:

var s = "foo"

Khuyến nghị:

s := "foo"

Tuy nhiên, trong một số trường hợp, sử dụng từ khóa var cho các giá trị mặc định có thể rõ ràng hơn.

Không được khuyến nghị:

func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

Khuyến nghị:

func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

nil là một slice hợp lệ

nil là một slice hợp lệ với độ dài là 0, có nghĩa:

  • Bạn không nên trả về một slice với độ dài là 0 một cách rõ ràng. Thay vào đó, hãy trả về nil.

Không nên:

if x == "" {
  return []int{}
}

Được khuyến nghị:

if x == "" {
  return nil
}
  • Để kiểm tra xem một slice có rỗng không, luôn sử dụng len(s) == 0 thay vì nil.

Không nên:

func isEmpty(s []string) bool {
  return s == nil
}

Được khuyến nghị:

func isEmpty(s []string) bool {
  return len(s) == 0
}
  • Slice với giá trị zero (slice được khai báo với var) có thể được sử dụng ngay mà không cần gọi make().

Không nên:

nums := []int{}
// hoặc nums := make([]int)

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

Được khuyến nghị:

var nums []int

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

Hãy nhớ rằng, mặc dù một slice nil là một slice hợp lệ, nó không bằng với một slice có độ dài là 0 (một cái là nil và cái kia không phải), và chúng có thể được xử lý khác nhau trong các tình huống khác nhau (ví dụ, serialization).

Thu hẹp Phạm vi biến

Nếu có thể, hãy cố gắng thu hẹp phạm vi của biến, trừ khi nó xung đột với quy tắc giảm sâu lồng.

Không nên:

err := os.WriteFile(name, data, 0644)
if err != nil {
 return err
}

Được khuyến nghị:

if err := os.WriteFile(name, data, 0644); err != nil {
 return err
}

Nếu kết quả của một cuộc gọi hàm bên ngoài câu lệnh if cần được sử dụng, đừng cố gắng thu hẹp phạm vi.

Không nên:

if data, err := os.ReadFile(name); err == nil {
  err = cfg.Decode(data)
  if err != nil {
    return err
  }

  fmt.Println(cfg)
  return nil
} else {
  return err
}

Được khuyến nghị:

data, err := os.ReadFile(name)
if err != nil {
   return err
}

if err := cfg.Decode(data); err != nil {
  return err
}

fmt.Println(cfg)
return nil

Tránh Sử Dụng Tham Số Trần

Các tham số không rõ ràng trong cuộc gọi hàm có thể làm tổn thương tính đọc. Khi ý nghĩa của tên tham số không rõ ràng, thêm chú thích theo kiểu C (/* ... */) cho các tham số.

Không nên:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)

Được khuyến nghị:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

Đối với ví dụ trên, một cách tiếp cận tốt hơn có thể là thay thế các kiểu bool bằng các kiểu tùy chỉnh. Như vậy, tham số có thể hỗ trợ nhiều hơn chỉ hai trạng thái (đúng/sai) trong tương lai.

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady Status = iota + 1
  StatusDone
  // Có thể chúng ta sẽ có một StatusInProgress trong tương lai.
)

func printInfo(name string, region Region, status Status)

Sử dụng chuỗi nguyên mẫu để tránh việc thoát

Go hỗ trợ việc sử dụng chuỗi nguyên mẫu, được chỉ định bằng " ` " để biểu diễn chuỗi nguyên mẫu. Trong các tình huống cần thoát, chúng ta nên sử dụng phương pháp này để thay thế cho các chuỗi thoát một cách khó đọc hơn một cách thủ công.

Chúng có thể trải dài qua nhiều dòng và bao gồm dấu ngoặc kép. Sử dụng các chuỗi này có thể tránh được các chuỗi thoát một cách khó đọc hơn.

Không nên:

wantError := "unknown name:\"test\""

Được khuyến nghị:

wantError := `unknown error:"test"`

Khởi tạo cấu trúc

Khởi tạo cấu trúc sử dụng tên trường

Khi khởi tạo một cấu trúc, hầu hết mọi khi cần phải chỉ định tên trường. Hiện tại điều này được bắt buộc bởi go vet.

Không khuyến nghị:

k := User{"John", "Doe", true}

Khuyến nghị:

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

Ngoại lệ: Khi có 3 hoặc ít hơn trường, tên trường trong các bảng kiểm tra có thể bị bỏ qua.

tests := []struct{
  op Operation
  want string
}{
  {Add, "add"},
  {Subtract, "subtract"},
}

Bỏ qua trường với giá trị không

Khi khởi tạo một cấu trúc với các trường có tên, trừ khi cung cấp ngữ cảnh có ý nghĩa, hãy bỏ qua các trường có giá trị không. Điều này có nghĩa là chúng ta sẽ tự động thiết lập chúng thành các giá trị không.

Không khuyến nghị:

user := User{
  FirstName: "John",
  LastName: "Doe",
  MiddleName: "",
  Admin: false,
}

Khuyến nghị:

user := User{
  FirstName: "John",
  LastName: "Doe",
}

Điều này giúp giảm thiểu rào cản đọc bằng cách loại bỏ các giá trị mặc định trong ngữ cảnh. Chỉ chỉ định các giá trị có ý nghĩa.

Bao gồm giá trị không khi tên trường cung cấp ngữ cảnh có ý nghĩa. Ví dụ, các trường kiểm tra trong một bảng kiểm tra định hướng có thể được lợi ích từ việc đặt tên trường, ngay cả khi chúng là giá trị không.

tests := []struct{
  give string
  want int
}{
  {give: "0", want: 0},
  // ...
}

Sử dụng var cho cấu trúc với giá trị không

Nếu tất cả các trường của một cấu trúc được bỏ qua trong khai báo, hãy sử dụng var để khai báo cấu trúc.

Không khuyến nghị:

user := User{}

Khuyến nghị:

var user User

Điều này phân biệt các cấu trúc với giá trị không từ các cấu trúc với các trường có giá trị không, tương tự như chúng ta ưa thích khi khai báo một slice trống.

Khởi tạo tham chiếu cấu trúc

Khi khởi tạo tham chiếu cấu trúc, sử dụng &T{} thay vì new(T) để làm cho nó nhất quán với việc khởi tạo cấu trúc.

Không khuyến nghị:

sval := T{Name: "foo"}

// không nhất quán
sptr := new(T)
sptr.Name = "bar"

Khuyến nghị:

sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

Khởi tạo Map

Đối với một map trống, sử dụng make(..) để khởi tạo nó, và map sẽ được điền thông qua chương trình. Điều này làm cho việc khởi tạo map khác khai báo về hình thức, và cũng thuận tiện cho phép thêm gợi ý về kích thước sau khi make.

Không khuyến nghị:

var (
  // m1 là an toàn khi đọc và viết;
  // m2 gây panics khi viết
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

Khuyến nghị:

var (
  // m1 là an toàn khi đọc và viết;
  // m2 gây panics khi viết
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

| Khai báo và khởi tạo trông rất giống nhau. | Khai báo và khởi tạo giống nhau rất khác nhau. |

Nếu có thể, cung cấp kích thước dung lượng map trong quá trình khởi tạo, xem Chi tiết Về Chỉ Định Kích Thước Map để biết chi tiết.

Ngoài ra, nếu map chứa một danh sách cố định các phần tử, sử dụng biểu thức map để khởi tạo map.

Không khuyến nghị:

m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3

Khuyến nghị:

m := map[T1]T2{
  k1: v1,
  k2: v2,
  k3: v3,
}

Hướng dẫn cơ bản là sử dụng biểu thức map để thêm một tập hợp cố định các phần tử trong quá trình khởi tạo. Nếu không, sử dụng make (và nếu có thể, chỉ định dung lượng map).

Chuỗi Định Dạng cho Hàm Printf-style

Nếu khai báo chuỗi định dạng của hàm theo kiểu Printf bên ngoài một hàm, hãy thiết lập nó dưới dạng const hằng số.

Điều này giúp go vet thực hiện phân tích tĩnh trên chuỗi định dạng.

Không khuyến nghị:

msg := "giá trị không đáng ngờ %v, %v\n"
fmt.Printf(msg, 1, 2)

Khuyến nghị:

const msg = "giá trị không đáng ngờ %v, %v\n"
fmt.Printf(msg, 1, 2)

Đặt tên cho các hàm theo kiểu Printf

Khi khai báo các hàm theo kiểu Printf, hãy đảm bảo rằng go vet có thể phát hiện và kiểm tra chuỗi định dạng.

Điều này có nghĩa là bạn nên sử dụng tên hàm theo kiểu Printf đã được xác định trước càng nhiều càng tốt. go vet sẽ kiểm tra những tên này mặc định. Để biết thêm thông tin, vui lòng xem Printf Family.

Nếu không thể sử dụng tên được xác định trước, kết thúc tên được chọn bằng f: Wrapf thay vì Wrap. go vet có thể yêu cầu kiểm tra các tên cụ thể theo kiểu Printf, nhưng tên phải kết thúc bằng f.

go vet -printfuncs=wrapf,statusf