ข้อบังคับพื้นฐานเกี่ยวกับมาตรฐานการเขียนโค้ด 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()
ในที่สุด หากไม่อาจหลีกเลี่ยงการใช้หรือถูกต้อง โค้ดควรพยายาม:
- ให้ครบถ้วนโดยไม่ว่าจะอยู่ในสภาพแวดล้อมหรือการเรียกทำงาน
- หลีกเลี่ยงการพึ่งพากำลังหรือผลข้างเคียงของฟังก์ชัน
init()
อื่น ๆ แม้ว่าลำดับของinit()
จะเป็นโดยชัดเจน โค้ดก็ยังสามารถเปลี่ยนแปลงได้ จึงทำให้ความสัมพันธ์ระหว่างinit()
ฟังก์ชันอาจทำให้โค้ดเปราะไปต่อข้อผิดพลาด - หลีกเลี่ยงการเข้าถึงหรือดำเนินการกับสถานะทั่วโลกหรือสภาพแวดล้อม เช่น ข้อมูลเครื่อง, ตัวแปรของสภาพแวดล้อม, ไดเรกทอรีทำงาน, พารามิเตอร์/ข้อมูลนำเข้าโปรแกรม ฯลฯ
- หลีกเลี่ยงการทำ 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",
})
ตามวิธีการกล่าวข้างต้น รูปแบบการตรึงระบบของโครงสร้างเป็นสัญญาระหว่างระบบที่แตกต่างกัน การทำการเปลี่ยนแปลงรูปแบบการตรึงระบบของโครงสร้าง (รวมถึงชื่อฟิลด์) จะทำให้สัญญาด้วยระหว่างระบบเสีย การระบุชื่อฟิลด์ในแท็กทำให้สัญญากลายเป็นชัดเจน และยังช่วยป้องกันการละเมิดสัญญาโดยไม่ได้ตั้งใจผ่านการเรีแฟกทอริ่งหรือการเปลี่ยนชื่อฟิลด์โดยบังเอิญ