اجتناب خطوط بسیار طولانی
از استفاده از خطوط کدی که نیازمند اسکرول افقی یا چرخش بیش از حد سند هستند، خودداری کنید.
ما توصیه میکنیم که حداکثر طول خط را به 99 کاراکتر محدود کنید. نویسندگان باید قبل از این محدودیت خط را شکسته ولی این یک قانون سخت نیست. اجازه داده شده است که کد بیشتر از این محدودیت باشد.
سازگاری
بعضی از استانداردهای مشخص شده در این سند براساس سنجشهای ذهنی، سناریوها یا متنوعیتها است. با این حال، مهمترین جنبه این است که سازگاری را حفظ کنید.
کد متناسب، آسانتر برای نگهداری است، بیشتر منطقی است، هزینه یادگیری کمتری دارد و زمانی راحتتری برای مهاجرت، بهروزرسانی و رفع خطاها مطالب میکند هنگامی که تضاد شیوههای تازهتر یا خطاهای جدید رخ دهد.
به عبارت دیگر، شامل کردن چندین سبک کاملاً متفاوت یا متناقض در یک مجموعه کده leads منجر به افزایش هزینه نگهداری، ناهمخوانی و تعصبهای شناور. تمام اینها مستقیماً منجر به کاهش سرعت، بررسیهای دردناک کد و افزایش تعداد باگها میشود.
هنگام اعمال این استانداردها به یک مجموعه کد، توصیه میشود که تغییرات را در سطح بسته (یا بزرگتر) اعمال کنید. اعمال چند سبک در سطح زیربسته، به رعایت موارد فوق خلاف میباشد.
گروهبندی اعلانهای مشابه
زبان Go پشتیبانی از گروهبندی اعلانهای مشابه است.
توصیه نمیشود:
import "a"
import "b"
توصیه میشود:
import (
"a"
"b"
)
این همچنین برای اعلانهای ثابت، متغیر و نوع نیز صدق میکند:
توصیه نمیشود:
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
)
تنها اعلانهای مرتبط را بههم گروه بندی کنید و از گروهبندی اعلانهای بیربط خودداری کنید.
توصیه نمیشود:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
EnvVar = "MY_ENV"
)
توصیه میشود:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const EnvVar = "MY_ENV"
هیچ محدودیتی برای استفاده از گروهبندی وجود ندارد. به عنوان مثال، میتوانید آنها را در داخل یک تابع استفاده کنید:
توصیه نمیشود:
func f() string {
red := color.New(0xff0000)
green := color.New(0x00ff00)
blue := color.New(0x0000ff)
...
}
توصیه میشود:
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
...
}
استثنا: اگر اعلانهای متغیر مجاور به متغیرهای دیگری باشند، بهویژه در معرفیهای محلی تابعی، آنها باید با هم گروهبندی شوند. این کار را حتی برای متغیرهای بیارتباطی که با هم معرفی شدهاند انجام دهید.
توصیه نمیشود:
func (c *client) request() {
caller := c.name
format := "json"
timeout := 5*time.Second
var err error
// ...
}
توصیه میشود:
func (c *client) request() {
var (
caller = c.name
format = "json"
timeout = 5*time.Second
err error
)
// ...
}
گروهبندی واردات
واردات باید به دو دسته تقسیم شوند:
- کتابخانه استاندارد
- کتابخانههای دیگر
به طور پیشفرض، این گروهبندی توسط goimports اعمال میشود. توصیه نمیشود:
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
توصیه میشود:
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
نام بسته
هنگام نامگذاری بسته، لطفاً این موارد را رعایت کنید:
- همه حروف کوچک، بدون حروف بزرگ یا زیرخط.
- در اغلب موارد، نیازی به تغییر نام در هنگام وارد کردن ندارید.
- کوتاه و مختصر. به یاد داشته باشید که نام بهصورت کامل در هر جایی که استفاده میشود، کاملاً کاربرد دارد.
- از جمعها خودداری کنید. به عنوان مثال، از
net/url
بهجایnet/urls
استفاده کنید. - از استفاده از "common"، "util"، "shared" یا "lib" خودداری کنید. اینها اطلاعات کافی نیستند.
نامگذاری توابع
ما به قانون اجتماع گولانگ پیرامون استفاده از MixedCaps برای نام توابع پایبند هستیم. یک استثنا برای گروهبندی مورد تستهای مرتبط وجود دارد، جایی که نام تابع ممکن است شامل زیرخط باشد، مانند: TestMyFunction_WhatIsBeingTested
.
اختصارات واردکنندهها
اگر نام بسته با عنصر آخر مسیر واردکننده مطابقت نداشته باشد، لازم است از یک اختصار واردکننده استفاده شود.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
در سایر موارد، استفاده از اختصارات واردکننده باید اجتناب شود مگر اینکه بین واردکنندهها تداخل مستقیم وجود داشته باشد. پیشنهاد نمیشود:
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
)
پیشنهاد میشود:
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
گروهبندی و ترتیب توابع
- توابع باید بهطور تقریبی بر اساس ترتیبی که فراخوانی میشوند، مرتب شوند.
- توابع در یک فایل باید بر اساس گیرنده گروهبندی شوند.
بنابراین، توابع صادرشده باید اولینبار در فایل ظاهر شوند، پس از تعاریف struct
، const
، و var
.
یک newXYZ()
/NewXYZ()
ممکن است پس از تعاریف نوعی ظاهر شود ولی قبل از باقی متدهایی از گیرنده.
زیرا توابع بر اساس گیرنده گروهبندی میشوند، توابع کلی ابزارها باید در انتهای فایل ظاهر شوند. پیشنهاد نمیشود:
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 {...}
کاهش تو در تویی
کد باید با کاهش تو در تویی تحت مدیریت بالا رفته و موارد خطا/ویژه بهطور زودترین امکان مدیریت شود و یا ادامهی حلقهی کارکردی را ادامه دهد. کاهش تو در تویی میزان کد در چند سطح را کاهش میدهد.
پیشنهاد نمیشود:
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()
}
else بیارتباط
اگر یک متغیر در هر دو شاخهی if تنظیم شود، میتوان با یک بیانحصار if تنها جایگزین کرد.
پیشنهاد نمیشود:
var a int
if b {
a = 100
} else {
a = 10
}
پیشنهاد میشود:
a := 10
if b {
a = 100
}
تعریف متغیرهای سطح بالا
در سطح بالا، از کلمه کلیدی استاندارد var
استفاده کنید. مگر اینکه نوع متغیر از نوع عبارت متفاوت باشد، نباید نوع را مشخص کنید.
پیشنهاد نمیشود:
var _s string = F()
func F() string { return "A" }
پیشنهاد میشود:
var _s = F()
// از آنجایی که F به طور صریح یک نوع رشتهای را برمیگرداند، ما نیازی به اشاره مستقیم نوع برای _s نداریم
func F() string { return "A" }
نوع را مشخص کنید اگر با نوع مورد نیاز عبارت تطابق نداشته باشد.
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F یک نمونه از نوع myError برمیگرداند، ولی ما نیاز داریم تا نوع error را داشته باشیم
استفاده از '_' به عنوان پیشوند برای ثابتها و متغیرهای بالادست غیرصادر
برای ثابتها و متغیرهای بالادست غیرصادر، آنها را با یک زیرخط _
پیشوند دهید تا وضاحتا نشان دهید که ماهیت گلوبال آنها در زمان استفاده است.
منطق پایه: متغیرها و ثابتهای بالادست دارای دامنهی بسته هستند. استفاده از نامهای عمومی میتواند به راحتی منجر به استفاده اشتباه ارزش مناسب در فایلهای دیگر شود.
توصیه نمیشود:
// foo.go
const (
defaultPort = 8080
defaultUser = "user"
)
// bar.go
func Bar() {
defaultPort := 9090
...
fmt.Println("Default port", defaultPort)
// اگر خط اول Bar() حذف شود، ما خطای کامپایل نخواهیم دید.
}
توصیه میشود:
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
)
استثنا: مقادیر خطاهای غیرصادر میتوانند از پیشوند err
بدون زیرخط استفاده نمایند. برای دیدن روش نامگذاری خطا مشاهده کنید.
جاسازی در ساختارها
انواع جاسازی شده (مانند mutex) باید در بالای لیست فیلدهای ساختار قرار گیرند و باید یک خط خالی وسط فیلدهای جاسازی شده از فیلدهای معمولی جدا کند.
توصیه نمیشود:
type Client struct {
version int
http.Client
}
توصیه میشود:
type Client struct {
http.Client
version int
}
جاسازی باید مزایای ملموسی ارائه دهد، مانند افزودن یا تقویت کارکرد به یک شیوه معقول. این باید بدون هیچ تأثیر معکوس بر روی کاربر استفاده شود. (همچنین ببینید: اجتناب از جاسازی انواع در ساختارهای عمومی)
استثناها: حتی در انواع غیرصادر، Mutex نباید به عنوان یک فیلد جاسازی شده استفاده شود. همچنین ببینید: مقدار صفر Mutex معتبر است.
جاسازی نباید:
- فقط برای زیبایی یا راحتی وجود داشته باشد.
- باعث سختتر شدن ساخت یا استفاده از نوع خارجی شود.
- بر روی ارزش صفر نوع خارجی تأثیر بگذارد. اگر نوع خارجی ارزش صفر مفیدی دارد، باید هنوز ارزش صفر مفیدی پس از جاسازی نوع داخلی وجود داشته باشد.
- دارای عاقبت از راه دور از فیلدها یا توابع مرتبط از نوع داخلی شود.
- انواع غیرصادر را آشکار کند.
- بر روی فرم کلونینگ نوع خارجی تأثیر بگذارد.
- API یا مفاهیم نوع را تغییر دهد.
- نوع داخلی را به یک فرم نامعمول جاسازی کند.
- جزئیات پیادهسازی از نوع خارجی را آشکارسازی کند.
- به کاربران اجازه دهد تا نوع داخلی را مشاهده یا کنترل کنند.
- رفتار عمومی توابع داخلی را به گونهای تغییر دهد که ممکن است کاربران را متعجب کند.
به طور خلاصه، بهصورت هوشمندانه و با هدف جاسازی کنید. یک آزمون خوب این است که "آیا تمام این متدها/فیلدهای صادر از نوع داخلی بهطور مستقیم به نوع خارجی اضافه میشوند؟" اگر پاسخ بله
یا خیر
باشد، نوع داخلی را جاسازی نکنید - به جای آن از فیلدها استفاده کنید.
توصیه نمیشود:
type A struct {
// اشتباه: A.Lock() و A.Unlock() اکنون در دسترس است
// فایده عملکردی ایجاد نمیکند و به کاربر اجازه میدهد جزئیات داخلی A را کنترل کند.
sync.Mutex
}
توصیه میشود:
type countingWriteCloser struct {
// خوب: Write() به سطح خارجی برای یک هدف خاص ارائه شده است
// و کار را به Write() از نوع داخلی واگذار میدهد.
io.WriteCloser
count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
w.count += len(bs)
return w.WriteCloser.Write(bs)
}
اعلانهای متغیرهای محلی
اگر متغیر به صورت صریح به یک مقدار تنظیم شود، برای استفاده از فرم اعلانکوتاه متغیر (:=
) باید استفاده شود.
توصیه نمیشود:
var s = "foo"
توصیه میشود:
s := "foo"
با این حال، در برخی موارد، استفاده از واژه کلیدی var
برای ارزشهای پیشفرض میتواند واضحتر باشد.
توصیه نمیشود:
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
یک برش معتبر است
nil
یک برش با طولی برابر با صفر است که به این معنی است:
- شما نباید به طور صریح یک برش با طول صفر را بازگردانید. به جای اینکه
nil
را بازگردانید.
توصیه نمیشود:
if x == "" {
return []int{}
}
توصیه شده:
if x == "" {
return nil
}
- برای بررسی اینکه یک برش خالی است، همیشه از
len(s) == 0
به جایnil
استفاده کنید.
توصیه نمیشود:
func isEmpty(s []string) bool {
return s == nil
}
توصیه شده:
func isEmpty(s []string) bool {
return len(s) == 0
}
- برشهای مقدار صفر (برشهایی که با
var
اعلان میشوند) بدون فراخوانیmake()
میتوانند بلافاصله استفاده شوند.
توصیه نمیشود:
nums := []int{}
// یا، 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)
}
به خاطر داشته باشید، اگرچه یک برش nil
یک برش معتبر است، اما برابر یک برش با طول صفر نیست (یکی nil
و دیگری نه) و ممکن است در شرایط مختلف به طرق متفاوتی (مثلاً، سریالسازی) قرار داده شوند.
محدود کردن دامنهی متغیرها
اگر ممکن است، سعی کنید دامنهی متغیرها را محدود کنید، مگر اینکه با قانون کاهش تو در تو در تضاد باشد.
توصیه نمیشود:
err := os.WriteFile(name, data, 0644)
if err != nil {
return err
}
توصیه شده:
if err := os.WriteFile(name, data, 0644); err != nil {
return err
}
اگر نتیجه فراخوانی تابع خارج از عبارت if نیاز به استفاده دارد، سعی نکنید دامنهی متغیر را محدود کنید.
توصیه نمیشود:
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
}
توصیه شده:
data, err := os.ReadFile(name)
if err != nil {
return err
}
if err := cfg.Decode(data); err != nil {
return err
}
fmt.Println(cfg)
return nil
اجتناب از پارامترهای ناشناخته
پارامترهای نامعلوم در فراخوانیهای تابع ممکن است خوانایی را تخریب کند. وقتی معنی نام پارامترها واضح نیست، به پارامترها با استفاده از نظریه C (/* ... */
) که شامل توضیحات است، اضافه کرد.
توصیه نمیشود:
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true)
توصیه شده:
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
برای مثال بالا، رویکرد بهتر ممکن است جایگزین کردن انواع bool
با نوعهای سفارشی باشد. به این ترتیب، پارامتر ممکن است در آینده بیشتر از دو وضعیت (درست/غلط) را پشتیبانی کند.
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady Status= iota + 1
StatusDone
// شاید ما در آینده یک StatusInProgress داشته باشیم.
)
func printInfo(name string, region Region, status Status)
استفاده از رشتههای خام برای اجتناب از اسکیپ
Go از رشتههای خام پشتیبانی میکند که توسط " ` " برای نمایش رشتههای خام نشان داده میشود. در صورت نیاز به اسکیپ، باید از این رویکرد برای جایگزینی رشتههای دستی که مشکلات خواندن را بوجود میآورند، استفاده کنیم.
این رشتهها میتوانند بر روی چندین خط بندی شده و شامل نقل قولها باشند. استفاده از این رشتهها میتواند از استفادههای دشوارتر برای خواندن رشتهها جلوگیری کند.
توصیه نمیشود:
wantError := "unknown name:\"test\""
توصیه شده:
wantError := `unknown error:"test"`
مقدمه سازی ساختارها
مقداردهی اولیه ساختارها با استفاده از نام فیلدها
پس از مقداردهی اولیه یک ساختار، تقریباً همیشه باید نام فیلدها مشخص شود. این در حال حاضر توسط go vet
اجرا میشود.
توصیه نمیشود:
k := User{"John", "Doe", true}
توصیه میشود:
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
مستثنی: وقتی تعداد فیلدها سه یا کمتر باشد، ممکن است نام فیلدها در جداول آزمون حذف شود.
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
حذف فیلدهای مقدار صفر در ساختارها
هنگام مقداردهی اولیه یک ساختار با فیلدهای نامگذاریشده، مگر اینکه زمینه معناداری فراهم شود، از فیلدهایی با مقدار صفر چشمپوشی نمایید. به عبارت دیگر، بگذارید اینها به طور خودکار به مقادیر صفر تنظیم شوند.
توصیه نمیشود:
user := User{
FirstName: "John",
LastName: "Doe",
MiddleName: "",
Admin: false,
}
توصیه میشود:
user := User{
FirstName: "John",
LastName: "Doe",
}
این کمک میکند تا با حذف مقادیر پیشفرض در زمینه، مانعهای خواندن را کاهش دهیم. تنها مقادیر معنادار را مشخص نمایید.
هنگامی که نام فیلدها زمینه معناداری فراهم میکنند، مقدار صفر را نیز در نظر بگیرید. به عنوان مثال، موارد آزمون در یک آزمون تحت جدول میتواند از تعیین نام فیلدها بهرهمند شوند، حتی اگر اینها مقدارهای صفر باشند.
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
استفاده از var
برای ساختارهای مقدار صفر
اگر تمامی فیلدهای یک ساختار در تعریف حذف شوند، از var
برای تعریف ساختار استفاده نمایید.
توصیه نمیشود:
user := User{}
توصیه میشود:
var user User
این امر، ساختارهای مقدار صفر را از آنچه که فیلدها دارای مقدار غیر صفر هستند متمایز مینماید، مشابه آنچه را که ما در تعریف یک بریده خالی ترجیح میدهیم.
مقداردهی اولیه مراجع ساختار
هنگام مقداردهی اولیه مراجع ساختار، بجای new(T)
از &T{}
استفاده نمایید تا با مقداردهی اولیه ساختار یکپارچه شود.
توصیه نمیشود:
sval := T{Name: "foo"}
// ناهمسان
sptr := new(T)
sptr.Name = "bar"
توصیه میشود:
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
مقداردهی اولیه نقشهها
برای یک نقشه خالی، از make(..)
برای مقداردهی اولیه استفاده نمایید و نقشه به صورت برنامهریزیشدن پر شود. این کار باعث میشود که مقداردهی اولیه نقشه از اعلام در ظاهر متفاوت شود و همچنین به راحتی امکان ارائه اشارههای اندازه برای make
فراهم میکند.
توصیه نمیشود:
var (
// m1 ایمن برای خواندن-نوشتن است؛
// m2 هنگام نوشتن خطا میدهد
m1 = map[T1]T2{}
var m2 map[T1]T2
)
توصیه میشود:
var (
// m1 ایمن برای خواندن-نوشتن است؛
// m2 هنگام نوشتن خطا میدهد
m1 = make(map[T1]T2)
var m2 map[T1]T2
)
| اعلام و مقداردهی اولیه به نظر خیلی مشابه میآید. | اعلام و مقداردهی اولیه به نظر خیلی متفاوت میآید. |
در صورت امکان، در زمان مقداردهی اولیه اندازه نقشه را نیز فراهم کنید، جهت جزئیات مربوط به ارائه نقشه از ظرفیت مشخص.
علاوه بر این، اگر نقشه شامل یک لیست ثابتی از عناصر است، از نگارهای نقشه برای مقداردهی اولیه نقشه استفاده نمایید.
توصیه نمیشود:
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
(و اگر امکان دارد، ظرفیت نقشه را مشخص نمایید) استفاده نمایید.
فرمت رشته برای توابع Printf-style
اگر رشته فرمت یک تابع به سبک Printf
را خارج از یک تابع اعلام نمایید، آن را به عنوان یک ثابت const
تنظیم نمایید.
این کار باعث میشود که go vet
قادر به انجام تجزیه و تحلیل استاتیک بر روی رشته فرمت شود.
توصیه نمیشود:
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
توصیه میشود:
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
نامگذاری توابع با الگوی Printf
هنگام اعلان توابع با الگوی Printf
، مطمئن شوید که go vet
قادر به تشخیص و بررسی رشته فرمت باشد.
این به معنی آن است که باید تا حد امکان از نامهای پیشفرض توابع با الگوی Printf
استفاده کنید. go vet
به طور پیشفرض اینها را بررسی میکند. برای اطلاعات بیشتر، به خانواده Printf مراجعه کنید.
اگر نامهای پیشفرض قابل استفاده نباشند، نام انتخابی را با f
به پایان برسانید: بهجای Wrap
از Wrapf
استفاده کنید. go vet
میتواند درخواست انتخاب نامهای خاص با الگوی Printf را بکند، اما نام باید با f
به پایان برسد.
go vet -printfuncs=wrapf,statusf