ข้อบังคับพื้นฐานเกี่ยวกับมาตรฐานการเขียนโค้ด Golang

ใช้ defer เพื่อปล่อยทรัพยากร

ใช้ defer เพื่อปล่อยทรัพยากรเช่นไฟล์และล็อค

ไม่แนะนำ:

p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// ง่ายที่จะลืมที่จะปลดล็อคเมื่อมีหลายทางออก

แนะนำ:

p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// อ่านง่ายขึ้น

ค่าต้นทุนของ defer ต่ำมาก ดังนั้นควรหลีกเลี่ยงเฉพาะเมื่อสามารถพิสูจน์ได้ว่าเวลาการประมวลผลของฟังก์ชันอยู่ในระดับนาโนเซกอนด์ การใช้ defer เพื่อปรับปรุงความอ่านค่าได้มีคุ้มค่า เพราะค่าใช้จ่ายของการใช้มันต่ำมาก สิ่งนี้เป็นจริงโดยเฉพาะสำหรับเมธอดที่ใหญ่ที่มีการใช้งานทรัพยากรมากกว่าแค่การเข้าถึงหน่วยความจำอย่างง่าย ที่ทรัพยากรการบริโภคของการคำนวณอื่น ๆ เหนื่อยยิ่งกว่า defer

ขนาดของช่อง

ขนาดของช่องควรเป็น 1 หรือเป็นแบบ unbuffered (ไม่มีการเก็บข้อมูลชั่วคราว) โดยปกติช่องมักจะเป็นแบบ unbuffered โดยค่าเริ่มต้นคือ 0 ขนาดอื่น ๆ จะต้องได้รับการตรวจสอบโดยเคร่งครัด เราต้องพิจารณาวิธีในการกำหนดขนาด พิจารณาสิ่งที่ป้องกันช่องจากการเขียนภายใต้สภาพการทำงานที่มีภาระมากและเมื่อถูกบล็อก และพิจารณาการเปลี่ยนแปลงที่เกิดขึ้นในตรรกะของระบบ ณ เวลานั้น

ไม่แนะนำ:

// ควรเพียงพอสำหรับการจัดการสถานการณ์ใด ๆ!
c := make(chan int, 64)

แนะนำ:

// ขนาด: 1
c := make(chan int, 1) // หรือ
// ช่อง unbuffered, ขนาดคือ 0
c := make(chan int)

Enums เริ่มต้นที่ 1

วิธีมาตรฐานในการชี้แนะ enums ใน Go คือการประกาศประเภทที่กำหนดเองและกลุ่ม const ที่ใช้ iota เนื่องจากค่าเริ่มต้นของตัวแปรคือ 0 enums ควรเริ่มต้นด้วยค่าที่ไม่ใช่ศูนย์โดยปกติ

ไม่แนะนำ:

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

ในบางกรณีการใช้ค่าศูนย์เป็นไปสำดับ (enums เริ่มต้นค่า 0) ที่ดีคือเช่นเมื่อค่าเริ่มต้นคือพฤติกรรมเริ่มต้นที่ดีที่สุด

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

การใช้ atomic

ใช้การดำเนินการแบบ atomic จากแพ็กเกจ sync/atomic เพื่อดำเนินการกับประเภทพื้นฐาน (int32, int64, ฯลฯ) เพราะสะพานมันง่ายที่จะลืมที่จะใช้การดำเนินการแบบ atomic ในการอ่านหรือปรับเปลี่ยนตัวแปร

go.uber.org/atomic เพิ่มความปลอดภัยของประเภทที่ใช้ในการดำเนินการนี้ โดยซ่อนประเภทฐาน ๆ ที่อยู่ข้างใน และรวมถึงการมีชนิด atomic.Bool ที่สะดวกด้วย

วิธีไม่แนะนำ:

type foo struct {
  running int32  // atomic
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // กำลังเรียกใช้งานอยู่แล้ว…
     return
  }
  // เริ่มต้น Foo
}

func (f *foo) isRunning() bool {
  return f.running == 1  // ชน!
}

วิธีแนะนำ:

type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // กำลังเรียกใช้งานอยู่แล้ว…
     return
  }
  // เริ่มต้น Foo
}

func (f *foo) isRunning() bool {
  return f.running.Load()
}

หลีกเลี่ยงการใช้ตัวแปร global ที่เปลี่ยนแปลงได้

ใช้วิธี dependency injection เพื่อหลีกเลี่ยงการเปลี่ยนแปลงตัวแปร global โดยสามารถใช้กับ function pointers และ value types อื่น ๆ

วิธีที่ไม่แนะนำ 1:

// sign.go
var _timeNow = time.Now
func sign(msg string) string {
  now := _timeNow()
  return signWithTime(msg, now)
}

วิธีที่แนะนำ 1:

// sign.go
type signer struct {
  now func() time.Time
}
func newSigner() *signer {
  return &signer{
    now: time.Now,
  }
}
func (s *signer) Sign(msg string) string {
  now := s.now()
  return signWithTime(msg, now)
}

วิธีที่ไม่แนะนำ 2:

// sign_test.go
func TestSign(t *testing.T) {
  oldTimeNow := _timeNow
  _timeNow = func() time.Time {
    return someFixedTime
  }
  defer func() { _timeNow = oldTimeNow }()
  assert.Equal(t, want, sign(give))
}

วิธีที่แนะนำ 2:

// sign_test.go
func TestSigner(t *testing.T) {
  s := newSigner()
  s.now = func() time.Time {
    return someFixedTime
  }
  assert.Equal(t, want, s.Sign(give))
}

หลีกเลี่ยงการใช้ตัวแปรที่ถูกกำหนดไว้ล่วงหน้า

Go Language Specification ระบุถึงตัวแปรที่ถูกกำหนดไว้ล่วงหน้าที่ไม่ควรใช้ในโปรเจกต์ Go นี้ ตัวแปรที่ถูกกำหนดไว้ล่วงหน้า ไม่ควรนำมาใช้ในชื่อต่าง ๆ ในขอบเขตที่แตกต่างกัน เนื่องจากการทำเช่นนี้จะทำให้ตัวแปรต้นฉบับถูกซ่อนไว้ในขอบเขตปัจจุบัน (หรือขอบเขตซ้อน) ซึ่งอาจทำให้โค้ดสับสน ในกรณีที่ดีที่สุดคอมไพเลอร์จะเกิดข้อผิดพลาด แต่ในกรณีที่แย่ที่สุด โค้ดเหล่านี้อาจนำเข้าข้อผิดพลาดที่ยากต่อการกู้คืน

วิธีที่ไม่แนะนำ 1:

var error string
// `error` ทำให้ตัวแปรที่ได้รับการกำหนดไว้ล่วงหน้าถูกซ่อนอัตโนมัติ

// หรือ

func handleErrorMessage(error string) {
    // `error` ทำให้ตัวแปรที่ได้รับการกำหนดไว้ล่วงหน้าถูกซ่อนอัตโนมัติ
}

วิธีที่แนะนำ 1:

var errorMessage string
// `error` ตอนนี้ชี้ไปที่ตัวแปรที่ไม่ได้ถูกซ่อน

// หรือ

func handleErrorMessage(msg string) {
    // `error` ตอนนี้ชี้ไปที่ตัวแปรที่ไม่ได้ถูกซ่อน
}

วิธีที่ไม่แนะนำ 2:

type Foo struct {
    // สมาชิกเหล่านี้จะไม่ได้ทับซ้อน แต่การนิยามใหม่ของ 'error' หรือ 'string' ตอนนี้กลายเป็นคำที่กำกวม
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` และ `f.error` ดูเหมือนกันทางภาพลักษณ์
    return f.error
}

func (f Foo) String() string {
    // `string` และ `f.string` ดูเหมือนกันทางภาพลักษณ์
    return f.string
}

วิธีที่แนะนำ 2:

type Foo struct {
    // `error` และ `string` ตอนนี้ชัดเจน
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

โปรดทราบว่าคอมไพเลอร์จะไม่สร้างข้อผิดพลาดเมื่อใช้ตัวแปรที่ถูกกำหนดไว้ล่วงหน้า แต่เครื่องมือเช่น go vet จะชี้แจงปัญหาเหล่านี้และปัญหาที่เกิดขึ้นโดยอัตโนมัติที่เกี่ยวข้อง

หลีกเลี่ยงการใช้ init()

พยายามหลีกเลี่ยงการใช้ init() ในที่สุด หากไม่อาจหลีกเลี่ยงการใช้หรือถูกต้อง โค้ดควรพยายาม:

  1. ให้ครบถ้วนโดยไม่ว่าจะอยู่ในสภาพแวดล้อมหรือการเรียกทำงาน
  2. หลีกเลี่ยงการพึ่งพากำลังหรือผลข้างเคียงของฟังก์ชัน init() อื่น ๆ แม้ว่าลำดับของ init() จะเป็นโดยชัดเจน โค้ดก็ยังสามารถเปลี่ยนแปลงได้ จึงทำให้ความสัมพันธ์ระหว่าง init() ฟังก์ชันอาจทำให้โค้ดเปราะไปต่อข้อผิดพลาด
  3. หลีกเลี่ยงการเข้าถึงหรือดำเนินการกับสถานะทั่วโลกหรือสภาพแวดล้อม เช่น ข้อมูลเครื่อง, ตัวแปรของสภาพแวดล้อม, ไดเรกทอรีทำงาน, พารามิเตอร์/ข้อมูลนำเข้าโปรแกรม ฯลฯ
  4. หลีกเลี่ยงการทำ I/O รวมถึงระบบไฟล์, เครือข่าย และการเรียกใช้ระบบ

โค้ดที่ไม่ประสงค์เหล่านี้อาจจะเหมาะสมกับการใช้กับ main() หรือที่อื่นในช่วงชีวิตของโปรแกรม หรือเขียนเป็นส่วนของ main() เอง โดยเฉพาะไลบรารีที่ตั้งใจที่จะถูกนำไปใช้โดยโปรแกรมอื่น ๆ ควรใส่ความสำคัญกับความครบถ้วนมากกว่าการดำเนินการ "init magic".

วิธีที่ไม่แนะนำ 1:

type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}

วิธีที่แนะนำ 1:

var _defaultFoo = Foo{
// ...
}
// หรือ, สำหรับความสามารถในการทดสอบที่ดีขึ้น:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}

วิธีที่ไม่แนะนำ 2:

type Config struct {
// ...
}
var _config Config
func init() {
// ไม่ดี: ขึ้นอยู่กับไดเรกทอรีปัจจุบัน
cwd, _ := os.Getwd()
// ไม่ดี: I/O
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}

วิธีที่แนะนำ 2:

type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// จัดการข้อผิดพลาด
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// จัดการข้อผิดพลาด
var config Config
yaml.Unmarshal(raw, &config)
return config
}

โดยพิจารณาข้อคิดที่แสดงข้างต้น ในบางกรณี การใช้ init() อาจเป็นที่ชอบหรือจำเป็นมากขึ้น ได้แก่:

  • ไม่สามารถแทนด้วยการกำหนดค่าแบบซับเป็นของนิพจน์ที่ซับซ้อน
  • การแทรกแฮ๊ก, เช่น database/sql, ลงทะเบียนชนิด ฯลฯ

แนะนำให้ระบุความจุของ slice เมื่อนำมาต่อเพิ่ม

ควรให้ลำดับความสำคัญในการระบุค่าความจุสำหรับ make() เมื่อทำการกำหนดค่าเริ่มต้นของ slice เพื่อที่จะนำมาต่อเพิ่ม

วิธีที่ไม่แนะนำ:

for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}

วิธีที่แนะนำ:

for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}

การใช้ Field Tags ในการตรึงระบบ Struct Serialization

เมื่อมีการทำการตรึงระบบไปที่ JSON, YAML, หรือรูปแบบอื่น ๆ ที่รองรับการตั้งชื่อฟิลด์ตามแท็ก ควรใช้แท็กที่เกี่ยวข้องสำหรับการแบนที่ถูกใช้เป็นคำอธิบาย

ไม่แนะนำ:

type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})

แนะนำ:

type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// ปลอดภัยต่อการเปลี่ยนชื่อ Name เป็น Symbol
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})

ตามวิธีการกล่าวข้างต้น รูปแบบการตรึงระบบของโครงสร้างเป็นสัญญาระหว่างระบบที่แตกต่างกัน การทำการเปลี่ยนแปลงรูปแบบการตรึงระบบของโครงสร้าง (รวมถึงชื่อฟิลด์) จะทำให้สัญญาด้วยระหว่างระบบเสีย การระบุชื่อฟิลด์ในแท็กทำให้สัญญากลายเป็นชัดเจน และยังช่วยป้องกันการละเมิดสัญญาโดยไม่ได้ตั้งใจผ่านการเรีแฟกทอริ่งหรือการเปลี่ยนชื่อฟิลด์โดยบังเอิญ