Skip to content

Uber's Go Style Guide Translation in Thai. Linked to the uber-go/guide as a part of contributions https://github.com/uber-go/guide

License

Notifications You must be signed in to change notification settings

pallat/uber-go-style-guide-th

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

92 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Uber Go Style Guide

Table of Contents

Introduction

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

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

คำแนะนำชุดนี้เดิมถูกเขียนขึ้นโดย Prashant Varanasi และ Simon Newton เพื่อช่วยให้เพื่อนร่วมงานเริ่มต้นเขียน Go กันได้เร็วขึ้น แต่หลังจากผ่านไปหลายปี มันก็ถูกแก้ไขเพิ่มเติมจากข้อเสนอแนะต่างๆที่ได้รับ

สำนวนการเขียน Go ในเอกสารนี้เป็นแบบฉบับที่ใช้กันที่ Uber ซึ่งปกติก็เป็นแนวทางเดียวกับการเขียน Go ทั่วไปอยู่แล้ว ซึ่งถ้าจะมีเพิ่มเติมจากภายนอกก็มาจากที่เหล่านี้:

  1. Effective Go
  2. The Go common mistakes guide

โค้ดทั้งหมดควรจะต้องไม่มี error ใดๆจาก golint และ go vet เราแนะนำให้คุณตั้งค่าใน editor ตามนี้:

  • Run goimports on save
  • Run golint and go vet to check for errors

คุณสามารถหาข้อมูลเพิ่มเติมเกี่ยวกับการเครื่องมือช่วยใน editors ได้จากที่นี่: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

Guidelines

Pointers to Interfaces

คุณแทบไม่จำเป็นต้องใช้พอยน์เตอร์เพื่อใส่ใน interface คุณแค่ส่งค่าตรงๆเข้าไป แต่จะส่งเป็นพอยน์เตอร์ก็ได้เช่นกัน

interface ประกอบไปด้วยสองสิ่ง:

  1. พอยน์เตอร์ ชี้ไปที่ type ของสิ่งที่เก็บ คุณจะคิดซะว่ามันเป็น "type" เลยก็ได้
  2. พอยน์เตอร์ ของสิ่งที่เก็บ ถ้าสิ่งนั้นเป็นพอยน์เตอร์ ก็จะเก็บตรงๆ แต่ถ้ามันเป็นค่าใดๆก็ตาม มันจะเก็บเป็นพอยน์เตอร์ของค่านั้นแทน

ถ้าคุณต้องการให้เมธอดแก้ไขค่าในตัวมันเองได้ด้วย นั่นคุณถึงจะต้องใช้พอยเตอร์

Receivers and Interfaces

เมธอดที่มีตัวรับเป็นค่าปกติ สามารถเรียกใช้บนตัวแปรพอยน์เตอร์ ได้เลย

For example,

type S struct {
  data string
}

func (s S) Read() string {
  return s.data
}

func (s *S) Write(str string) {
  s.data = str
}

sVals := map[int]S{1: {"A"}}

// คุณเรียกใช้ Read ได้อย่างเดียว
sVals[1].Read()

// This will not compile:
//  sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// คุณเรียกใช้ได้ทั้ง Read และ Write ผ่านพอยน์เตอร์
sPtrs[1].Read()
sPtrs[1].Write("test")

และในทางกลับกัน interface ยอมให้คุณแทนที่ด้วยพอยน์เตอร์ได้ แม้ว่าเมธอดจะใช้ตัวรับเป็นแค่ค่าปกติ

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}

var i F
i = s1Val
i = s1Ptr
i = s2Ptr

// โค้ดด้านล่างนี้ไม่สามารถทำงานได้ เนื่องจาก s2Val เป็นค่าปกติ ในขณะที่ตัวรับในเมธอดไม่ใช่ค่าปกติแต่เป็นพอยน์เตอร์

Effective Go เขียนเรื่องนี้ไว้ได้ดีมากในเรื่อง Pointers vs. Values

Zero-value Mutexes are Valid

ค่า zero-value ของ sync.Mutex และ sync.RWMutex สามารถใช้งานได้โดยไม่ต้อง initial นั่นแปลว่าคุณแทบไม่ต้องใช้พอยน์เตอร์กับ mutex เลย

BadGood
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()

ถ้าคุณใช้ struct ด้วยพอยเตอร์ mutex จะสามารถเป็นแบบ ไม่มีพอยน์เตอร์ให้

struct ที่ไม่ได้เปิดเผยสู่ภายนอกที่ใช้ mutex ปกป้องฟิลด์ในตัวมันเอง อาจจะฝัง mutext ไว้แบบนี้

type smap struct {
  sync.Mutex // ใช้เฉพาะ type ที่ไม่เปิดเผยสู่ภายนอก

  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]
}
การฝัง ใช้กับ type ที่อยู่ภายใน หรือ type ที่ต้องการทำตัวเองเป็น Mutext interface สำหรับ type ที่ต้องการเปิดเผยสู่ภายนอก ให้ใช้แบบ ฟิลด์ ภายใน struct

Copy Slices and Maps at Boundaries

Slices และ maps เก็บของเป็นพอยน์เตอร์ ดังนั้นให้ระมัดระวังเวลาที่จะ copy ค่าเหล่านี้

Receiving Slices and Maps

ต้องจำไว้นะว่า map หรือ slice ที่คุณรับเข้ามาเป็นอากิวเม้นต์ ก็ถูกคนที่ใช้มันแก้ไขได้ ถ้าคุณเก็บข้อมูลชนิดที่มันอ้างถึงกัน

Bad Good
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// คุณต้องการจะแก้ไขค่า d1.trips หรือเปล่า?
trips[0] = ...
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// ตอนนี้เราก็สามารถแก้ไขค่า trips[0] โดยไม่กระทบ d1.trips ได้แล้ว
trips[0] = ...

Returning Slices and Maps

ในทางกลับกัน ให้ระมัดระวังการแก้ไขค่าไปที่ map หรือ slices ที่เปิดเผยสู่ภายนอกในระดับภายใน

BadGood
type Stats struct {
  mu sync.Mutex
  counters map[string]int
}

// Snapshot คืน ค่า ณ เวลาปัจจุบัน
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  return s.counters
}

// snapshot ไม่ถูกป้องกันโดย mutex ดังนั้น
// ใครก็ตามที่เข้ามาถึง snapshot อาจจะเกิดการแย่งของกัน
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 to Clean Up

ใช้ defer เพื่อ คืน resource หรือทรัพยากร ที่จองหรือนำไปใช้งานต่างๆเช่น ไฟล์ และ อะไรที่ถูกล็อคไว้

BadGood
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

// more readable

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

Channel Size is One or None

Channels ปกติควรมีขนาดอยู่ที่ 1 หรือไม่มีบัฟเฟอร์เลย โดยค่าตั้งต้น channels จะเป็นแบบไม่มีบัฟเฟอร์ และมีขนาดเป็นศูนย์ ขนาดอื่นๆ ขึ้นอยู่กับวิจารณญาณ ขึ้นอยู่กับว่า จะป้องกันการเติมของ ในขณะที่กำลังโหลด และมีการเขียน อย่างไร

BadGood
// หวังว่าจะพอสำหรับทุกคนนะ!
c := make(chan int, 64)
// ให้ขนาดเป็นหนึ่ง
c := make(chan int, 1) // or
// ไม่มีตัวกันชนเลย หรือมีขนาดเท่ากับศูนย์
c := make(chan int)

Start Enums at One

วิธีมาตรฐานในการทำ enum ใน go คือการ สร้าง type ขึ้นมาเอง หรือประกาศเป็นกลุ่ม const ด้วยการใช้ iota ซึ่งโดยปกติตัวแปรจะมีค่าตั้งต้นเป็น 0 เสมอ เพราะฉะนั้นเวลาที่คุณจะทำ enum ควรจะเริ่มด้วยค่าที่ไม่ใช่ศูนย์นะ

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

มันก็มีบางกรณีเหมือนกันที่การใช้ศูนย์อาจจะเหมาะสมกว่า ขึ้นอยู่กับสถานการณ์

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

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

Error Types

การสร้าง error ทำได้หลายวิธี:

  • errors.New เมื่อสร้างจากสตริงง่ายๆ
  • fmt.Errorf เมื่อต้องการจัดรูปแบบข้อความ
  • สร้าง type ที่ implement Error เมธอด
  • หุ้ม error ด้วยการใช้ "pkg/errors".Wrap

เมื่อจะทำการคืน errors ทางเลือกไหนถึงจะดีที่สุด ลองตั้งคำถามดูว่า:

  • นี่เป็น error ที่ต้องการข้อมูลเพิ่มเป็นพิเศษไหม ถ้าไม่ ก็ใช้ errors.New ก็น่าจะพอแล้ว
  • คนที่จะเอา error นี้ไปใช้ต่อ เขาต้องการจะสืบหาไหมว่า นี่เป็นความผิดพลาดแบบไหน ถ้าใช่ คุณควรสร้าง type ที่มีเมธอด Error() ขึ้นมาใช้เองจะดีกว่า
  • คุณต้องการจะบอกคนอื่นไหมว่า นี่เป็น error ที่เกิดขึ้นตรงไหน ถ้าใช่ ลองใช้ตัวนี้ดู section on error wrapping
  • ในกรณีอื่นๆ fmt.Errorf ก็เป็นตัวเลือกที่ดี

ถ้าผู้เรียก ต้องการสืบว่านี่เป็น error อะไร และคุณอยากจะสร้างมันด้วย errors.New ก็ขอให้ ทำให้มันเป็นตัวแปรดีกว่า

BadGood
// package foo

func Open() error {
  return errors.New("could not open")
}

// package bar

func use() {
  if err := foo.Open(); err != nil {
    if err.Error() == "could not open" {
      // handle
    } else {
      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 err == foo.ErrCouldNotOpen {
    // handle
  } else {
    panic("unknown error")
  }
}

ถ้าคุณมี error ที่ผู้เรียกต้องการสืบหาว่าเป็นแบบไหน แต่คุณก็อยากจะเพิ่มข้อมูลลงไปในนั้น (ไม่ใช่ค่าคงที่) ถ้างั้นคุณก็น่าจะสร้าง type มาใช้เอง

BadGood
func open(file string) error {
  return fmt.Errorf("file %q not found", file)
}

func use() {
  if err := open(); err != nil {
    if strings.Contains(err.Error(), "not found") {
      // handle
    } else {
      panic("unknown error")
    }
  }
}
type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

func open(file string) error {
  return errNotFound{file: file}
}

func use() {
  if err := open(); err != nil {
    if _, ok := err.(errNotFound); ok {
      // handle
    } else {
      panic("unknown error")
    }
  }
}

ขอให้ระมัดระวังการเปิดเผย error type ที่คุณสร้างมันขึ้นมาออกสู่ภายนอกโดยตรง เราแนะทำให้คุณเปิดฟังก์ชันที่ใช้เช็ค type ของ error นี้ออกไปแทนจะดีกว่า

// package foo

type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

func IsNotFoundError(err error) bool {
  _, ok := err.(errNotFound)
  return ok
}

func Open(file string) error {
  return errNotFound{file: file}
}

// package bar

if err := foo.Open("foo"); err != nil {
  if foo.IsNotFoundError(err) {
    // handle
  } else {
    panic("unknown error")
  }
}

Error Wrapping

มีสามวิธีที่จะบอกให้ผู้ที่เรียกใช้รู้ว่าการทำงานผิดพลาด:

  • คืน error เดิมๆออกไปเลย ถ้าคุณไม่ต้องการเพิ่มคำอธิบายใดๆ และอยากให้เห็น error ดิบๆแบบนั้น
  • เพิ่มคำอธิบายลงไปด้วยการใช้ "pkg/errors".Wrap และใช้ "pkg/errors".Cause เวลาที่ต้องการถอดเอาเฉพาะ error เดิมออกมา
  • ใช้ fmt.Errorf ถ้าผู้เรียกไม่อยากรู้ว่าเป็น error แบบไหนให้ชัดเจน

เราแนะนำให้เพิ่มคำอธิบายลงไปถ้าทำได้ แทนที่จะให้เห็น error แบบคลุมเครือเช่น "connection refused" แล้วเพิ่มคำอธิบายให้มีประโยชน์มากกว่าลงไป เช่น "call service foo: connection refused"

เวลาที่คุณจะเพิ่มคำอธิบายใน error ให้ใช้ประโยคที่กระชับ แล้วไม่ต้องใส่คำเวิ่นเว้อเช่น "failed to" ไม่งั้นเวลามันผ่านหลายๆชั้นแล้วมันจะดูเป็นคำขยะ:

BadGood
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %s", err)
}
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %s", err)
}
failed to x: failed to y: failed to create new store: the error
x: y: new store: the error

แต่ไม่ว่ายัง เวลาที่ error ถูกส่งไปที่ระบบอื่น มันควรมีความชัดเจนในข้อความ (ตัวอย่างเช่น ติดป้ายว่า err หรือใช้คำนำหน้า "Failed" ตอนที่ลง logs)

See also Don't just check errors, handle them gracefully.

Handle Type Assertion Failures

การรับค่าเดียวตอนที่ทำ type assertion มันอาจจะ panic ถ้า type มันไม่ถูก ดังนั้นให้ใช้สำนวนแบบ "comma ok" เสมอ

BadGood
t := i.(string)
t, ok := i.(string)
if !ok {
  // handle the error gracefully
}

Don't Panic

โค้ดที่จะขึ้น Production อย่าใช้ panics เพราะ Panic เป็นตัวหลักของการเกิด cascading failures ถ้ามันเกิด error ขึ้น ก็ให้ฟังก์ชันคืน error ออกไป ให้คนที่เรียกเขาไปตัดสินใจจัดการเอาเองเถิด

BadGood
func foo(bar string) {
  if len(bar) == 0 {
    panic("bar must not be empty")
  }
  // ...
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  foo(os.Args[1])
}
func foo(bar string) error {
  if len(bar) == 0 {
    return errors.New("bar must not be empty")
  }
  // ...
  return nil
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  if err := foo(os.Args[1]); err != nil {
    panic(err)
  }
}

Panic/recover ไม่ใช่วิธีการจัดการ error เพราะโปรแกรมจะ panic เฉพาะเมื่อเกิดเหตุที่คาดไม่ถึงเช่น ไปอ้างถึงอะไรก็แล้วแต่ กับค่า nil เว้นแค่จะเป็นช่วงเตรียมของก่อนเริ่มโปรแกรม ถ้าเกิดเหตุที่ไม่คาดคิดก็ควรจะหยุดการทำงานของโปรแกรมไปเลย

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

แม้กระทั่งใน tests ก็แนะนำให้่ใช้ t.Fatal หรือ t.FailNow มากกว่าการทำให้มัน panic เพื่อบอกให้เทสรู้ว่าเกิดข้อผิดพลาด

BadGood
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  panic("failed to set up test")
}
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  t.Fatal("failed to set up test")
}

Use go.uber.org/atomic

ตัวทำ automic ในแพ็กเกจ [sync/automic] ใช้ได้กับ type ดิบๆ (int32, int64, etc.) เราเลยลืมที่จะใช้มันเวลาจะอ่านหรือแก้ไขค่าตัวแปร

go.uber.org/atomic ได้เพิ่ม type ที่ปลอดภัยเข้าไปอีก โดยซ่อน type จริงๆไว้ข้างล่าง นอกจากนี้ยังเพิ่ม type atomic.Bool เพื่อให้สะดวกขึ้นอีก

BadGood
type foo struct {
  running int32  // atomic
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // already running…
     return
  }
  // start the Foo
}

func (f *foo) isRunning() bool {
  return f.running == 1  // race!
}
type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // already running…
     return
  }
  // start the Foo
}

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

Performance

คำแนะนำโดยตรงเกี่ยวกับประสิทธิภาพ คือทำเฉพาะส่วนที่เป็น hot path (ส่วนที่ถูกเรียกใช้งานหนักๆ)

Prefer strconv over fmt

เมื่อต้องการแปลงชนิดไปมา กับสตริง ให้ใช้ strconv จะเร็วกว่าใช้ fmt

BadGood
for i := 0; i < b.N; i++ {
  s := fmt.Sprint(rand.Int())
}
for i := 0; i < b.N; i++ {
  s := strconv.Itoa(rand.Int())
}
BenchmarkFmtSprint-4    143 ns/op    2 allocs/op
BenchmarkStrconv-4    64.2 ns/op    1 allocs/op

Avoid string-to-byte conversion

อย่าสร้าง slices ของ byte จากสตริงในลูป ให้ทำครั้งเดียวพอ

BadGood
for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}
BenchmarkBad-4   50000000   22.2 ns/op
BenchmarkGood-4  500000000   3.25 ns/op

Prefer Specifying Map Capacity Hints

ถ้าทำได้ ให้บอกใบ้ขนาดให้กับ map ตอนที่เรียก make()

make(map[T1]T2, hint)

การใบ้ค่าความจุกับ make() อย่างน้อยพยายามให้มันใกล้เคียงที่สุด ตอนที่สร้าง map จะช่วยลดเวลาตอนที่ต้องเพิ่มขนาดมันทีหลัง ซึ่งอันที่จริงการใส่ความจุแบบนี้ก็ไม่รับประกันว่ามันจะไม่เสียเวลา เพราะบางทีการเพิ่มของเข้าไปก็อาจจะเกิดกระบวนการจองหน่วยความจำได้ ทั้งๆที่ก็ได้ให้ความจุไปก่อนแล้ว

BadGood
m := make(map[string]os.FileInfo)

files, _ := ioutil.ReadDir("./files")
for _, f := range files {
    m[f.Name()] = f
}
files, _ := ioutil.ReadDir("./files")

m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
    m[f.Name()] = f
}

m ถูกสร้างโดยไม่ใบ้ขนาดให้มัน ซึ่งมันอาจจะทำให้ต้องเสียเวลาจังหวะที่จะจองหน่วยความจำ

m ถูกสร้างโดยใบ้ขนาดให้ด้วย ซึ่งจะเสียเวลาจองหน่วยความจำนิดเดียว

Style

Be Consistent

คำแนะนำบางส่วนที่ระบุในเอกสารชุดนี้ วัดผลได้จริง เว้นไว้แต่เพียง พฤติกรรม บริบท หรือหัวข้อต่างๆ

นอกเหนือจากที่กล่าวมาก็คือ ทำให้เป็นจังหวะเดียวกัน

โค้ดที่ลายมือเดียวกัน มันดูแลรักษาง่าย มันง่ายที่จะเข้าใจ ไม่ทำให้เสียเวลาต้องมานั่งแกะ แล้วถ้าแก้ไขย้ายที่มันก็ยังทำได้ง่ายกว่า รวมถึงตอนแก้บั๊กด้วย

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

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

Group Similar Declarations

Go สนับสนุนการจัดกลุ่มการการประกาศที่เป็นพวกเดียวกัน

BadGood
import "a"
import "b"
import (
  "a"
  "b"
)

การทำแบบนี้ยังสามารถทำได้กับการประกาศ constant ตัวแปร และการประกาศ type

BadGood
const a = 1
const b = 2



var a = 1
var b = 2



type Area float64
type Volume float64
const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

จัดกลุ่มเฉพาะสิ่งที่สัมพันธ์กัน อย่าไปทำกับอะไรที่ไม่เกี่ยวข้องกัน

BadGood
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  ENV_VAR = "MY_ENV"
)
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const ENV_VAR = "MY_ENV"

การจัดกลุ่มสามารถทำในฟังก์ชันก็ได้ ดังแสดงในตัวอย่าง

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

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

  ...
}

Import Group Ordering

แบ่งกลุ่มการอิมพอร์ตเป็นสองชุด:

  • Standard library
  • Everything else

การจัดกลุ่มนี้ goimports ทำให้โดยปกติอยู่แล้ว

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

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

Package Names

เวลาจะประกาศชื่อแพ็กเกจ ให้เลือกแบบนี้:

  • ใช้ตัวอักษรเล็กทั้งหมด ไม่มีตัวใหญ่ หรือขีดล่าง
  • ไม่เปลี่ยนชื่อมันตอนที่ผู้ใช้ import มันเข้าไป
  • สั้นและกระชับ เพราะมันจะถูกอ้างถึงในทุกที่จะมาเรียกใช้
  • ไม่ต้องทำเป็นพหูพจน์
  • อย่าตั้งชื่อ "common", "util", "shared" ชื่อพวกนี้มันห่วย เพราะไม่ได้ช่วยให้เรารู้อะไรเลย

ดูเพิ่มเติมได้ที่ Package Names และ Style guideline for Go packages

การตั้งชื่อฟังก์ชัน

เราทำแบบเดียวกับที่ชุมชนคนเขียน go ทำกันด้วยการใช้ MixedCaps for function names (การผสมตัวอักษรเล็กและใหญ่) ยกเว้นเฉพาะเวลาเขียนเทส สามารถใช้ขีดล่างได้ เพื่อจัดกลุ่มการทดสอบที่สัมพันธ์กัน ตัวอย่างเช่น TestMyFunction_WhatIsBeingTested.

Import Aliasing

การตั้งชื่อแฝงให้แพ็กเกจที่ import ทำเมื่อชื่อแพ็กเกจที่นำเข้ามาไม่ตรงกับส่วนสุดท้ายของพาร์ท

import (
  "net/http"

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

ในกรณีอื่นๆ การตั้งชื่อแฝงให้การ import ไม่ควรทำ เว้นเสียแต่ว่ามันจะไปซ้ำกันกับแพ็กเกจอื่น

BadGood
import (
  "fmt"
  "os"


  nettrace "golang.net/x/trace"
)
import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

Function Grouping and Ordering

  • ควรเรียงฟังก์ชันตามลำดับการเรียกใช้
  • ฟังก์ชันในไฟล์ควรจัดกลุ่มตาม receiver

ฟังก์ชันที่เปิดเผยไปข้างนอกควรอยู่ในส่วนแรกๆของไฟล์ หลังการประกาศ struct, const, var

ฟังก์ชันแบบนี้ newXYZ()/NewXYZ() ควรอยู่หลังการประกาศ type แต่อยู่ก่อนเมธอดที่ใช้ type นี้เป็นตัว receiver

พอฟังก์ชันถูกจัดกลุ่มแบบนี้ พวกฟังก์ชันที่ใช้งานทั่วไปก็ควรไปอยู่ส่วนท้ายๆของไฟล์

BadGood
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{}
}
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 {...}

Reduce Nesting

โค้ดควรลดความยุ่งเหยิงด้วยการจัดการ error ก่อนแล้วรีเทิร์นออกไป หรือไปเริ่มต้นลูปใหม่ ให้เร็วที่สุด เพื่อลดโค้ดที่ซ้อนกันหลายๆชั้น

BadGood
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)
  }
}
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()
}

Unnecessary Else

ถ้าตัวแปรจะถูกกำหนดค่าทั้งใน if และ else มันควรจะเหลือแค่ if ก็ได้

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

Top-level Variable Declarations

การใช้คียเวิร์ด var ไม่ต้องบอก type ก็ได้ เว้นเสียแต่ว่ามันจะคืน type ไม่ตรงกับที่ต้องการ

BadGood
var _s string = F()

func F() string { return "A" }
var _s = F()
// Since F already states that it returns a string, we don't need to specify
// the type again.

func F() string { return "A" }

ระบุ type ถ้า type ที่ได้รับมาไม่ตรงกับที่อยากได้

type myError struct{}

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

func F() myError { return myError{} }

var _e error = F()
// F returns an object of type myError but we want error.

Prefix Unexported Globals with _

ตั้งชื่อขึ้นต้นด้วย ขีดล่าง เวลาประกาศด้วย vars และ consts ให้ตัวแปรที่ไม่เปิดเผยสู่ภายนอก เพื่อทำให้ชัดเจนว่ามันถูกใช้เป็น global อยู่ภายในแพ็กเกจ

ข้อยกเว้น: ตัวแปร error ที่ไม่เปิดเผยสู่ภายนอก ควรตั้งขื่อขึ้นต้นด้วย err

หลักการและเหตุผล: ตัวแปรที่ประกาศไว้ตั้งแต่ต้น และพวก constants มีขอบเขตในแพ็กเกจ เพราะฉะนั้น การตั้งชื่อแบบกลางๆ มันจะทำให้เกิดเรื่องไม่คาดคิดได้ ทำให้ได้ค่าผิดในไฟล์อื่นได้ง่ายมาก

BadGood
// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Default port", defaultPort)

  // We will not see a compile error if the first line of
  // Bar() is deleted.
}
// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

Embedding in Structs

type ที่ถูกฝังไว้ (เช่น mutexes) ควรอยู่บนสุดของรายการใน struct และควรเว้นบรรทัดว่างๆไว้สักบรรทัด

BadGood
type Client struct {
  version int
  http.Client
}
type Client struct {
  http.Client

  version int
}

Use Field Names to Initialize Structs

คุณควรระบุชื่อฟิลด์เสมอเมื่อประกาศตัวแปรจาก struct ซึ่งตอนนี้เวลานี้ถูกบังคับโดย go vet เรียบร้อยแล้ว

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

ข้อยกเว้น: ชื่อฟิลด์ อาจจะ ละไว้ได้ในตารางการทดสอบถ้ามันมี 3 ฟิลด์หรือน้อยกว่า

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

Local Variable Declarations

การประกาศตัวแปรแบบสั้น (:=) ควรถูกใช้เมื่อต้องการกำหนดค่าให้ตัวแปรอยู่แล้ว

BadGood
var s = "foo"
s := "foo"

อย่างไรก็ดี บางกรณีการปล่อยให้มันเป็นค่าเริ่มต้นก็อาจจะชัดเจนกว่า ด้วยการใช้คีย์เวิร์ด var Declaring Empty Slices ตัวอย่างเช่น

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

nil is a valid slice

nil เป็นค่าที่เหมาะสมที่จะใช้แทน slice ขนาด 0 หมายความว่า

  • คุณไม่ควรคืน slice ที่มีขนาดเท่ากับศูนย์ออกไปตรงๆ แต่ให้คืน nil ออกไปแทน

    BadGood
    if x == "" {
      return []int{}
    }
    if x == "" {
      return nil
    }
  • การตรวจสอบว่า slice นั้นว่างเปล่าหรือไม่ ให้ใช้ len(s) == 0 อย่าไปตรวจสอบ nil

    BadGood
    func isEmpty(s []string) bool {
      return s == nil
    }
    func isEmpty(s []string) bool {
      return len(s) == 0
    }
  • zero value (slice ที่ประกาศด้วย var) สามารถใช้งานได้เลย โดยไม่ต้อง make() ก่อน

    BadGood
    nums := []int{}
    // or, nums := make([]int)
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }
    var nums []int
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }

Reduce Scope of Variables

ถ้ามีโอกาสลดขอบเขตของตัวแปรก็ควรทำ แต่อย่าไปลดมันถ้ามันขัดแย้งกับ Reduce Nesting

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

ถ้าคุณต้องการผลลัพธ์ของฟังก์ชันไปใช้หลัง if ต่อ งั้นคุณก็ไม่ควรลดขอบเขตมัน

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

  fmt.Println(cfg)
  return nil
} else {
  return err
}
data, err := ioutil.ReadFile(name)
if err != nil {
   return err
}

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

fmt.Println(cfg)
return nil

Avoid Naked Parameters

พารามิเตอร์เปลือยๆที่ใส่ไปตอนที่เรียกฟังก์ชัน มันอ่านยาก ให้เพิ่มคอมเม้นท์แบบ C-style ลงไป (/* ... */) ให้ความหมายชัดเจนขึ้น

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

printInfo("foo", true, true)
// func printInfo(name string, isLocal, done bool)

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

แต่มันก็ยังไม่ดีที่สุด เราควรแทนที่ type bool ที่เปลือยๆอยู่นี้ด้วยการสร้าง type ขึ้นมาให้มันอ่านง่ายขึ้น และยังรองรับหากในอนาคตต้องการมีมากกว่าสองสถานะ (true/false)

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady = iota + 1
  StatusDone
  // Maybe we will have a StatusInProgress in the future.
)

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

Use Raw String Literals to Avoid Escaping

Go สนับสนุน raw string literals ซึ่งสามารถใส่ได้หลายบรรทัดรวมทั้งเครื่องหมายคำพูดได้ด้วย ซึ่งการใช้แบบนี้เพื่อป้องกันการทำ hand-escaped เพราะมันจะทำให้อ่านยาก

BadGood
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`

Initializing Struct References

ใช้ &T{} แทนการใช้ new(T) เมื่อต้องการสร้างตัวแปรแบบอ้างอิงจาก struct จะดูดีกว่า

BadGood
sval := T{Name: "foo"}

// inconsistent
sptr := new(T)
sptr.Name = "bar"
sval := T{Name: "foo"}

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

Initializing Maps

เสนอให้ใช้ make(..) เพื่อสร้าง maps ว่างๆ และเอาไปเขียนโปรแกรมต่อได้ ซึ่งมันทำการประกาศตัวแปรให้พร้อมใช้งานดูมีความต่างจากการประกาศเฉยๆ และมันยังทำให้ง่ายต่อการเพิ่มการใบ้ขนาดให้ในภายหลังด้วย

BadGood
var (
  // m1 is safe to read and write;
  // m2 will panic on writes.
  m1 = map[T1]T2{}
  m2 map[T1]T2
)
var (
  // m1 is safe to read and write;
  // m2 will panic on writes.
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

การประกาศให้พร้อมใช้งาน กับการประกาศแล้วยังไม่พร้อมใช้งาน ดูคล้ายๆกัน

การประกาศให้พร้อมใช้งาน กับการประกาศแล้วยังไม่พร้อมใช้งาน ดูแตกต่างกัน

ถ้าทำได้ ก็ให้ใบ้ความจุตอนที่ประกาศ maps ด้วยคำสั่ง make() ลองดูที่ Prefer Specifying Map Capacity Hints สำหรับข้อมูลเพิ่มเติม

หรือในทางกลับกัน ถ้า map นั้นจะต้องเก็บค่าที่แน่นอน ก็ให้ใช้การประกาศด้วยปีกกาได้เลย

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

กฎพื้นฐานของนิ้วหัวแม่มือก็คือ ใช้ปีกกาประกาศเมื่อต้องใส่ค่าคงที่ลงไปตั้งแต่ต้น ไม่เช่นนั้นก็ใช้ make (และใส่การใบ้ความจุถ้าทำได้)

Format Strings outside Printf

ถ้าคุณประกาศการจัดรูปแบบสตริงสำหรับใช้กับฟังก์ชัน Printf-style ให้ทำเป็น const

มันจะช่วยให้ go vet ได้วิเคราะห์การจัดรูปแบบให้

BadGood
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

Naming Printf-style Functions

เมื่อคุณประกาศฟังก์ชัน Printf-style ช่วยทำให้มั่นใจว่า go vet จะสามารถตรวจเจอมันและจะได้ตรวจสอบรูปแบบสตริงได้

หมายความว่า คุณควรใช้ชื่อที่ตั้งเผื่อไว้แล้วตามสไตล์ Printf ถ้าทำได้ go vet จะได้ตรวจสอบได้เอง ดูรายละเอียดเพิ่มเติมได้ที่ Printf family

ถ้าการใช้ชื่อที่ตั้งเผื่อไว้ ไม่ใช่ทางเลือกของคุณ งั้นก็ให้ตั้งชื่อลงท้ายด้วย f: เช่น Wrapf ไม่ใช่ Wrap เฉยๆ โดยสามารถบอกให้ go vet ตรวจสอบฟังก์ชันสไตล์ Printf ได้ แต่มันจะต้องลงท้ายด้วยตัว f เท่านั้น

$ go vet -printfuncs=wrapf,statusf

See also go vet: Printf family check.

Patterns

Test Tables

ใช้การทดสอบที่ขับเคลื่อนด้วยตาราง ด้วย subtests เพื่อหลีกเลี่ยงการเขียนโค้ดซ้ำๆ เวลาที่เราเทสด้วยลอจิกแบบเดิมหลายๆครั้ง

BadGood
// func TestSplitHostPort(t *testing.T)

host, port, err := net.SplitHostPort("192.0.2.0:8000")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "8000", port)

host, port, err = net.SplitHostPort("192.0.2.0:http")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "http", port)

host, port, err = net.SplitHostPort(":8000")
require.NoError(t, err)
assert.Equal(t, "", host)
assert.Equal(t, "8000", port)

host, port, err = net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t, "1", host)
assert.Equal(t, "8", port)
// func TestSplitHostPort(t *testing.T)

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  {
    give:     "192.0.2.0:8000",
    wantHost: "192.0.2.0",
    wantPort: "8000",
  },
  {
    give:     "192.0.2.0:http",
    wantHost: "192.0.2.0",
    wantPort: "http",
  },
  {
    give:     ":8000",
    wantHost: "",
    wantPort: "8000",
  },
  {
    give:     "1:8",
    wantHost: "1",
    wantPort: "8",
  },
}

for _, tt := range tests {
  t.Run(tt.give, func(t *testing.T) {
    host, port, err := net.SplitHostPort(tt.give)
    require.NoError(t, err)
    assert.Equal(t, tt.wantHost, host)
    assert.Equal(t, tt.wantPort, port)
  })
}

ตารางการทดสอบช่วยทำให้ง่ายต่อการเพิ่มบริบท (context) ให้ error message ลด code ที่ซ้ำซ้อน และง่ายต่อการเพิ่มชุดการทดสอบ (test case)

เราปฏิบัติตามประเพณีนิยมด้วยการใช้ slice ของ struct แล้วตั้งชื่อว่า tests และแต่ละ test case ให้ชื่อ tt และระบุชื่อให้ input และ output ในแต่ละ test case ด้วยการตั้งชื่อขึ้นต้นว่า give และ want

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  // ...
}

for _, tt := range tests {
  // ...
}

Functional Options

Functional options เป็นรูปแบบที่ใช้ประกาศ type Option เพื่อบันทึกข้อมูลลงไปใน struct ภายใน จากนั้นให้รับตัวแปรแบบ varidic เข้ามาเป็นตัวเลือก และจัดการตามข้อมูลที่บันทึกไว้ใน options ที่เป็น struct ภายใน

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

BadGood
// package db

func Connect(
  addr string,
  timeout time.Duration,
  caching bool,
) (*Connection, error) {
  // ...
}

// Timeout and caching must always be provided,
// even if the user wants to use the default.

db.Connect(addr, db.DefaultTimeout, db.DefaultCaching)
db.Connect(addr, newTimeout, db.DefaultCaching)
db.Connect(addr, db.DefaultTimeout, false /* caching */)
db.Connect(addr, newTimeout, false /* caching */)
type options struct {
  timeout time.Duration
  caching bool
}

// Option overrides behavior of Connect.
type Option interface {
  apply(*options)
}

type optionFunc func(*options)

func (f optionFunc) apply(o *options) {
  f(o)
}

func WithTimeout(t time.Duration) Option {
  return optionFunc(func(o *options) {
    o.timeout = t
  })
}

func WithCaching(cache bool) Option {
  return optionFunc(func(o *options) {
    o.caching = cache
  })
}

// Connect creates a connection.
func Connect(
  addr string,
  opts ...Option,
) (*Connection, error) {
  options := options{
    timeout: defaultTimeout,
    caching: defaultCaching,
  }

  for _, o := range opts {
    o.apply(&options)
  }

  // ...
}

// Options must be provided only if needed.

db.Connect(addr)
db.Connect(addr, db.WithTimeout(newTimeout))
db.Connect(addr, db.WithCaching(false))
db.Connect(
  addr,
  db.WithCaching(false),
  db.WithTimeout(newTimeout),
)

See also,

About

Uber's Go Style Guide Translation in Thai. Linked to the uber-go/guide as a part of contributions https://github.com/uber-go/guide

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published