Podstawowe wytyczne dotyczące standardów kodowania w Golang
Użyj defer do zwalniania zasobów
Użyj defer do zwalniania zasobów, takich jak pliki i zamki.
Nie zalecane:
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// Łatwo jest zapomnieć o odblokowaniu, gdy istnieje wiele sekcji zwracających
Zalecane:
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// Więcej czytelne
Nakład defera jest niezwykle niski, dlatego powinno się go unikać tylko wtedy, gdy można udowodnić, że czas wykonania funkcji jest na poziomie nanosekund. Używanie defera do poprawy czytelności jest ważne, ponieważ koszt ich użycia jest znikomy. Dotyczy to zwłaszcza większych metod, które obejmują więcej niż proste dostępy do pamięci, gdzie zużycie zasobów innych obliczeń znacznie przewyższa użycie defera.
Rozmiar kanału powinien wynosić 1 lub być bez buforu
Kanały powinny zazwyczaj mieć rozmiar 1 lub być bez buforu. Domyślnie kanały są bez buforu, o rozmiarze zero. Każdy inny rozmiar musi być ściśle przeglądany. Należy rozważyć, w jaki sposób określić rozmiar, zastanowić się, co zapobiega zapisowi kanału przy dużym obciążeniu oraz w momencie blokady, oraz zastanowić się, jakie zmiany zachodzą w logice systemu w takich sytuacjach.
Nie zalecane:
// Powinno wystarczyć, aby poradzić sobie w każdej sytuacji!
c := make(chan int, 64)
Zalecane:
// Rozmiar: 1
c := make(chan int, 1) // lub
// Kanał bez buforu, rozmiar wynosi 0
c := make(chan int)
Enumy zaczynają się od 1
Standardową metodą wprowadzania enumów w Go jest deklaracja niestandardowego typu i grupy const, która wykorzystuje iota. Ponieważ domyślną wartością zmiennych jest 0, enumy zazwyczaj powinny zaczynać się od wartości niezerowej.
Nie zalecane:
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
Zalecane:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3
W niektórych przypadkach używanie wartości zerowej ma sens (enumy zaczynające się od zera), na przykład, gdy wartość zerowa jest idealnym zachowaniem domyślnym.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
Używanie atomic
Używaj operacji atomicznych z pakietu sync/atomic do operacji na typach podstawowych (int32
, int64
, itp.), ponieważ łatwo jest zapomnieć o użyciu operacji atomicznych do odczytu lub modyfikacji zmiennych.
Pakiet go.uber.org/atomic dodaje bezpieczeństwo typów do tych operacji, ukrywając bazowy typ. Dodatkowo zawiera wygodny typ atomic.Bool
.
Nie zalecane podejście:
type foo struct {
running int32 // atomic
}
func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// już uruchomione...
return
}
// uruchom Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // wyścig!
}
Zalecane podejście:
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
// już uruchomione...
return
}
// uruchom Foo
}
func (f *foo) isRunning() bool {
return f.running.Load()
}
Unikaj używania mutowalnych zmiennych globalnych
Użyj podejścia wstrzykiwania zależności, aby uniknąć zmian zmiennych globalnych. Dotyczy to wskaźników funkcji oraz innych typów wartości.
Nie zalecane podejście 1:
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
Zalecane podejście 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)
}
Nie zalecane podejście 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))
}
Zalecane podejście 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))
}
Unikaj korzystania z wcześniej zadeklarowanych identyfikatorów
Specyfikacja języka Go opisuje kilka wcześniej zadeklarowanych identyfikatorów, które nie powinny być używane w projektach Go. Te wcześniej zadeklarowane identyfikatory nie powinny być ponownie używane jako nazwy w różnych kontekstach, ponieważ spowoduje to ukrycie oryginalnych identyfikatorów w bieżącym zakresie (lub jakimkolwiek zagnieżdżonym zakresie), co potencjalnie prowadzi do zamieszania w kodzie. W najlepszym przypadku kompilator wygeneruje błąd, a w najgorszym przypadku taki kod może wprowadzić potencjalne, trudne do naprawienia błędy.
Nie zalecane praktyki 1:
var error string
// `error` niejawnie zasłania wbudowany identyfikator
// lub
func handleErrorMessage(error string) {
// `error` niejawnie zasłania wbudowany identyfikator
}
Rekomendowana praktyka 1:
var errorMessage string
// `error` teraz odnosi się do niezasłoniętego wbudowanego identyfikatora
// lub
func handleErrorMessage(msg string) {
// `error` teraz odnosi się do niezasłoniętego wbudowanego identyfikatora
}
Nie zalecane praktyki 2:
type Foo struct {
// Mimo że te pola technicznie nie zasłaniają, ponowne definiowanie ciągów `error` lub `string` staje się teraz dwuznaczne.
error error
string string
}
func (f Foo) Error() error {
// `error` i `f.error` wizualnie wyglądają podobnie
return f.error
}
func (f Foo) String() string {
// `string` i `f.string` wizualnie wyglądają podobnie
return f.string
}
Rekomendowana praktyka 2:
type Foo struct {
// `error` i `string` są teraz jasno określone.
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
Zauważ, że kompilator nie generuje błędów podczas korzystania z wcześniej zadeklarowanych identyfikatorów, ale narzędzia takie jak go vet
poprawnie zwracają uwagę na te i inne związane z nimi problemy.
Unikaj używania init()
Postaraj się jak najbardziej unikać używania init()
. Gdy użycie init()
jest nieuniknione lub preferowane, kod powinien starać się:
- Zapewnić kompletność niezależnie od środowiska programu lub wywołania.
- Unikać polegania na kolejności lub skutkach ubocznych innych funkcji
init()
. Choć kolejnośćinit()
jest jawnie określona, kod może ulec zmianie, więc związek między funkcjamiinit()
może uczynić kod łamliwym i podatnym na błędy. - Unikać dostępu lub manipulowania globalnymi lub środowiskowymi stanami, takimi jak informacje o maszynie, zmienne środowiskowe, katalogi robocze, parametry/ wejścia programu, itp.
- Unikać wejścia/wyjścia, w tym systemów plików, sieci i wywołań systemowych.
Kod, który nie spełnia tych wymagań, może należeć jako część wywołania main()
(lub gdzieś indziej w cyklu życia programu) lub być napisany jako część samego main()
. W szczególności biblioteki przeznaczone do użycia przez inne programy powinny zwracać szczególną uwagę na kompletność, a nie na wykonywanie "magicznych" operacji "init".
Nie zalecane podejście 1:
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
Zalecane podejście 1:
var _defaultFoo = Foo{
// ...
}
// lub, dla lepszej testowalności:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
Nie zalecane podejście 2:
type Config struct {
// ...
}
var _config Config
func init() {
// Źle: zależne od bieżącego katalogu
cwd, _ := os.Getwd()
// Źle: I/O
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
Zalecane podejście 2:
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// obsłuż błąd
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// obsłuż błąd
var config Config
yaml.Unmarshal(raw, &config)
return config
}
Biorąc pod uwagę powyższe rozważania, w niektórych przypadkach init()
może być bardziej preferowany lub konieczny, w tym:
- Nie można go przedstawić jako pojedynczego przypisania złożonego wyrażenia.
- Możliwe do wstawienia haczyki, takie jak
database/sql
, rejestry typów, itp.
Preferuj określenie pojemności w przypisaniu sekwencji
Zawsze priorytetyzuj określenie wartości pojemności dla make()
podczas inicjalizowania sekwencji do dodania.
Nie zalecane podejście:
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Zalecane podejście:
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Używanie etykiet pól w serializacji struktury
Podczas serializacji do formatu JSON, YAML lub innego formatu obsługującego nazewnictwo pól na podstawie etykiet, należy używać odpowiednich etykiet do adnotacji.
Nie zalecane:
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
Zalecane:
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Bezpieczne jest zmienić Name na Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
W teorii format serializacji struktury jest umową między różnymi systemami. Dokonanie zmian w formie serializacji struktury (w tym nazw pól) zerwie tę umowę. Określenie nazw pól w etykietach czyni umowę jawną i pomaga również zapobiec przypadkowym naruszeniom umowy poprzez refaktoryzację lub zmianę nazw pól.