We write in Go like in Google. Best Practices – Part One

Go Best Practices

This document is part of the documentation for go style in Google. He is not neither normativenor canonicalthis is an addition to “style guide“. See details in Obzor.

About document

Here are recommendations on best practices for applying the requirements of the Style Guide for Go. This guide covers general and common cases, but cannot be applied to every particular case. A discussion of alternatives is, where possible, included in the text of the guidance, along with guidance on when they are applicable and when they are not.

The full style guide documentation is described in review.


naming

Function and method names

Avoid repetition

When naming a function or method, consider the context in which the name is read. To avoid unnecessary repetitions at the call point, follow the guidelines below:

 // Плохо:
 package yamlconfig

 func ParseYAMLConfig(input string) (*Config, error)
 // Хорошо:
 package yamlconfig

 func Parse(input string) (*Config, error)

  • In methods, do not repeat the name of the method receiver:

    // Плохо:
    func (c *Config) WriteConfigTo(w io.Writer) (int64, error)

    // Хорошо:
    func (c *Config) WriteTo(w io.Writer) (int64, error)

  • Do not repeat variable names that were specified as parameters:

    // Плохо:
    func OverrideFirstWithSecond(dest, source *Config) error

    // Хорошо:
    func Override(dest, source *Config) error

  • Don’t repeat names and return types:

    // Плохо:
    func TransformYAMLToJSON(input *Config) *jsonconfig.Config

    // Хорошо:
    func Transform(input *Config) *jsonconfig.Config

However, if it is necessary to distinguish between functions with a similar name, it is acceptable to include additional information in the name:

// Хорошо:
func (c *Config) WriteTextTo(w io.Writer) (int64, error)
func (c *Config) WriteBinaryTo(w io.Writer) (int64, error)

Name resolution

There are other rules for naming methods and functions:

  • The names of value-returning functions must be formed from nouns:

    // Хорошо:
    func (c *Config) JobName(key string) (value string, ok bool)

    So the prefix Get in the names of functions and methods should be avoided:

    // Плохо:
    func (c *Config) GetJobName(key string) (value string, ok bool)

  • The names of functions that perform actions must be formed from verbs:

    // Хорошо:
    func (c *Config) WriteDetail(w io.Writer) (int64, error)

  • Identical functions that differ only in the type used must end with the type name at the end of the function name:

    // Хорошо:
    func ParseInt(input string) (int, error)
    func ParseInt64(input string) (int64, error)
    func AppendInt(buf []byte, value int) []byte
    func AppendInt64(buf []byte, value int64) []byte

    If there is a “primary” version, the type in its name can be omitted:

    // Хорошо:
    func (c *Config) Marshal() ([]byte, error)
    func (c *Config) MarshalText() (string, error)



    Test duplicates of packages and types

At naming test packages and types, especially test doubles, several rules apply. By its function, a test double can be a stub (stub), a mock object (fake), a mock object (mock) or a test spy (spy).

In these examples, as a rule, we are talking about stubs. If in your case this is an imitation or something else, update the names accordingly.

Let’s say you have a custom package that represents running code:

package creditcard

import (
 "errors"

 "path/to/money"
)

// ErrDeclined указывает на то, что эмитент отклоняет платеж.
var ErrDeclined = errors.New("creditcard: declined")

// Карта содержит информацию о кредитной карте, такую ​​как ее эмитент,
// срок действия и лимит.
type Card struct {

}

// Сервис позволяет совершать операции с кредитными картами внешних
// платежных систем, такие как взимание платы, авторизация, возмещение и подписка.
type Service struct {

}

func (s *Service) Charge(c *Card, amount money.Money) error { /* опущено */ }

Creating test helper packages

You want to create a package with test takes for another package. Let’s use the expression package creditcardtaken from the above code.

Option: You can introduce a new Go package for the test by building it from a running package. In order not to confuse these packages, we add the word to the package name test:(“creditcard” + “test”):

// Хорошо:
package creditcardtest

Unless otherwise noted, all examples in the following sections will be written within package creditcardtest.

Simple example

You want to add a set of test takes for Service. Because the Card is actually a stub like the Protocol Buffer message, it doesn’t need any special handling in the tests, which means no duplication is needed. If you expect test takes to only apply to one type (for example, Service), you can name the takes succinctly:

// Хорошо:
import (
 "path/to/creditcard"
 "path/to/money"
)

// Stub заглушает creditcard.Service и не предоставляет собственного поведения.
type Stub struct{}

func (Stub) Charge(*creditcard.Card, money.Money) error { return nil }

Unlike the name StubService or worse, StubCreditCardServicesuch a choice of name is openly welcomed, since the name of the base package and its domain types provide enough information about creditcardtest.Stub.

And finally, if the package is created in Bazel, make sure the new rule is go_library for this package is marked as testonly:

# Good:
go_library(
 name = "creditcardtest",
 srcs = ["creditcardtest.go"],
 deps = [
 ":creditcard",
 ":money",
 ],
 testonly = True,
)

This is a standard approach that is quite understandable to most engineers.

See also:

Behavior of multiple test takes

When your tests require more than one stub option (for example, you need a stub that always throws an error), it’s a good idea to name them according to the behavior being modeled. For example, Stub can be renamed to AlwaysCharges and insert a new stub – AlwaysDeclines:

// Хорошо:
// AlwaysCharges — заглушка creditcard.Service, симулирующая успех операции.
type AlwaysCharges struct{}

func (AlwaysCharges) Charge(*creditcard.Card, money.Money) error { return nil }

// AlwaysDeclines — заглушка creditcard.Service, симулирующая отклонение платежа.
type AlwaysDeclines struct{}

func (AlwaysDeclines) Charge(*creditcard.Card, money.Money) error {
 return creditcard.ErrDeclined
}

Multiple takes for multiple types

Let’s pretend that package creditcard contains multiple types and for each it makes sense to create duplicates as shown below for Service And StoredValue:

package creditcard

type Service struct {

}

type Card struct {

}

// StoredValue управляет кредитными балансами клиентов. Структура 
// применима, когда возвращенный товар зачисляется на локальный счет 
// клиента, а не обрабатывается эмитентом кредита. По этой причине он
// реализован как отдельный сервис.
type StoredValue struct {

}

func (s *StoredValue) Credit(c *Card, amount money.Money) error { /* опущено */ }

In this case, it is advisable to give test takes more explicit names:

// Хорошо:
type StubService struct{}

func (StubService) Charge(*creditcard.Card, money.Money) error { return nil }

type StubStoredValue struct{}

func (StubStoredValue) Credit(*creditcard.Card, money.Money) error { return nil }

Local variables in tests

If variables refer to test takes, their names must clearly distinguish between takes and working types, given the context. Let’s say you want to test working code:

package payment

import (
 "path/to/creditcard"
 "path/to/money"
)

type CreditCard interface {
 Charge(*creditcard.Card, money.Money) error
}

type Processor struct {
 CC CreditCard
}

var ErrBadInstrument = errors.New("payment: instrument is invalid or expired")

func (p *Processor) Process(c *creditcard.Card, amount money.Money) error {
 if c.Expired() {
 return ErrBadInstrument
 }
 return p.CC.Charge(c, amount)
}

Test take CreditCard named “spy” is next to worker types, so a prefix before the name will help clarify:

// Хорошо:
package payment

import "path/to/creditcardtest"

func TestProcessor(t *testing.T) {
 var spyCC creditcardtest.Spy

 proc := &Processor{CC: spyCC}

 // объявления опущены: карта и сумма
 if err := proc.Process(card, amount); err != nil {
 t.Errorf("proc.Process(card, amount) = %v, want %v", got, want)
 }

 charges := []creditcardtest.Charge{
 {Card: card, Amount: amount},
 }

 if got, want := spyCC.Charges, charges; !cmp.Equal(got, want) {
 t.Errorf("spyCC.Charges = %v, want %v", got, want)
 }
}

It’s clearer than without the prefix.

// Плохо:
package payment

import "path/to/creditcardtest"

func TestProcessor(t *testing.T) {
 var cc creditcardtest.Spy

 proc := &Processor{CC: cc}

 // объявления опущены: карта и сумма
 if err := proc.Process(card, amount); err != nil {
 t.Errorf("proc.Process(card, amount) = %v, want %v", got, want)
 }

 charges := []creditcardtest.Charge{
 {Card: card, Amount: amount},
 }

 if got, want := cc.Charges, charges; !cmp.Equal(got, want) {
 t.Errorf("cc.Charges = %v, want %v", got, want)
 }
}

Shading

In this section, two informal terms are used – these are concealment (stomping) And shading. They are not part of the official terminology of the Go language.

Like many other languages, Go has mutable variables. This means that the assignment statement changes the value of the variable.

// Хорошо:
func abs(i int) int {
 if i < 0 {
 i *= -1
 }
 return i
}

At short declaration of variables using the operator := sometimes a new variable is not created. We call it variable hiding (stomping). It is quite acceptable when we no longer need the initial value of the variable.

// Хорошо:
// innerHandler — хелпер для обработчика запросов, самостоятельно
// отправляющий запросы другим бэкендам.
func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse {
 // Unconditionally cap the deadline for this part of request handling.
 ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
 defer cancel()
 ctxlog.Info("Capped deadline in inner request")

 // Код здесь больше не имеет доступа к исходному контексту.
 // Это хороший стиль, если при первом написании такого кода вы ожидаете,
 // что даже по мере роста кода ни одна корректная операция не должна
 // использовать (возможно, неограниченный) исходный контекст, 
 // предоставленный вызывающей стороной.

 // ...
}

But be careful with short variable declarations in the new scope. It results in the creation of a new variable. We call it variable shading. The code after the end of the block refers to the initial value. Below is an erroneous attempt to reduce the deadline for execution (deadline) on a condition:

// Плохо:
func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse {
 // Попытка ограничить срок условием.
 if *shortenDeadlines {
 ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
 defer cancel()
 ctxlog.Info(ctx, "Capped deadline in inner request")
 }

 // БАГ: "ctx" здесь снова означает контекст, предоставленный 
 // вызывающей стороной.
 // Приведенный выше код с ошибками скомпилирован, потому что и ctx, и 
 // Cancel использовались внутри оператора if.

 // ...
}

Correct code might look like this:

// Хорошо:
func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse {
 if *shortenDeadlines {
 var cancel func()
 // Применяется простое присвоение, = а не :=.
 ctx, cancel = context.WithTimeout(ctx, 3*time.Second)
 defer cancel()
 ctxlog.Info(ctx, "Capped deadline in inner request")
 }
 // ...
}

Here we have hidden (stomping) the variable. Since there is no new variable, the type assigned must match the type of the initial variable. With shadowing, we introduce a completely new object, which may be of a different type. Shading can be helpful, but here for clarity always use the new name.

Using out of scope variables with names that repeat the names of source packages is not a good idea, because unused functions of such a package become inaccessible. Conversely, when choosing a package name, avoid names that might require renaming on import or shadowing good variable names on the client side.

// Плохо:
func LongFunction() {
 url := "https://example.com/"
 // Oops, now we can't use net/url in code below.
}

Util packages

Go packages have a name given in the package declaration, separate from the import path. The package name is more important for readability than the path.

Go package names should be related to their functionality.. Name the package with just one word: util, helper, common or similar is usually not the best solution (however, this word can become part name). Uninformative names make code difficult to read, and if used often, can even cause unreasonable import conflicts.

But the call point might look like this:

// Хорошо:
db := spannertest.NewDatabaseFromFile(...)

_, err := f.Seek(0, io.SeekStart)

b := elliptic.Marshal(curve, x, y)

A rough idea of ​​the functionality of each object can be obtained without a list of imports (cloud.google.com/go/spanner/spannertest, io And crypto/elliptic). With less meaningful names, the code would look like this:

// Плохо:
db := test.NewDatabaseFromFile(...)

_, err := f.Seek(0, common.SeekStart)

b := helper.Marshal(curve, x, y)

Package size

If you’ve been wondering how big a package should be in Go, and whether you should put similar types in one package or separate them into different ones, you should start your search for the answer with the article in blog about package names in Go. The article is devoted not only to names. It has helpful hints, discussions, and quotes on a variety of topics.

Here are some other thoughts and comments.

As a package on one page, users see godoc, and all methods exported by types from the package are grouped by their type; godoc also groups constructors together with their return types. If client codeit is likely that two values ​​of different types will need to interact, it may be convenient for the user to store them in one package.

Code within a package can access unexported identifiers within the package. If you have multiple related types, implementation which are closely related, placing them in the same package allows you to achieve communication between them without polluting the public API with details about this relationship.

However, if you put the entire project in a single package, the package becomes unreasonably bloated. When a part of a project is conceptually different from other parts, it’s easier to isolate the authentic part into a separate package. The short package name known to clients, together with the exported type name, form a friendly identifier, for example bytes.Buffer, ring.New. IN this blog article you will find more examples.

The Go style allows flexibility in file resizing: while maintaining a package, code can be moved within the package from one file to another without harming callers [частей кода]. But, as experience shows, neither a single file with many thousands of lines, nor many small files are the optimal solution. There is no “one type, one file” rule in Go. The file structure is organized well enough for the programmer who edits it to understand what to look for in which file. At the same time, the files should be small enough to make it easier to find something in them. The standard library often splits the source code of a package into multiple files, grouping related code into a single file. A good example would be the package code bytes. In packages with voluminous accompanying documentation, one doc.go can be singled out for package documentation and his ads. In general, you don’t need to include anything else there.

In the Google codebase and in Bazel projects, the location of the Go code directories is different from the location of the code in open source Go projects: you can have multiple targets (targets) go_library in one directory. If you plan to make the project public, it’s a good rationale to give each package its own directory.

See also:

Imports

Protocols and stubs

Importing protocol libraries differs from importing other Go imports in terms of handling cross-language specifics. The rule for renamed proto imports is based on the package creation rule:

  • Suffix pb usually used within the rules go_proto_library.
  • Suffix grpc usually used within the rules go_grpc_library.

The prefix usually consists of one or two letters:

// Хорошо:
import (
 fspb "path/to/package/foo_service_go_proto"
 fsgrpc "path/to/package/foo_service_go_grpc"
)

If the package uses only one protocol (proto) or the package is tightly bound to the protocol, then the prefix can be omitted:

import ( pb "path/to/package/foo\_service\_go\_proto" grpc "path/to/package/foo\_service\_go\_grpc" )

If the protocol uses generic or uninformative characters, as well as non-obvious abbreviations, a short word can become a prefix:

// Хорошо:
import (
 mapspb "path/to/package/maps_go_proto"
)

Here, when the connection of the code with the cards is not obvious, mapspb.Address easier to understand than mpb.Address.

Import order

As a rule, imports are grouped into two or more blocks in the following sequence:

  1. Standard library objects like "fmt".
  2. Other imports like “/path/to/somelib”.
  3. Optional imports of protobuf protocol buffers, e.g. fpb "path/to/foo_go_proto".
  4. Optional imports of side effects, e.g. _ "path/to/package".

If the file does not have a group for one of the above optional categories, the corresponding import is included in the project’s import group.

As a rule, any clear, understandable grouping of imports is acceptable. Team members can choose to group gRPC imports separately from protobuf imports.

For code containing only two required groups, i.e. standard library and other imports, the tool goimports produces a result that meets the requirements of this guide.

However, about optional groups goimports knows nothing, and therefore annuls them. If optional groups are used, code authors and maintainers should pay attention to whether the grouping meets the specified requirements.

However, any approach that provides a complete and consistent grouping in accordance with the specified requirements is acceptable.

This is only a small part of the document, we will publish its continuation soon. In the meantime, you can start practicing and gaining useful experience in our courses:

Data Science and Machine Learning

Python, web development

Mobile development

Java and C#

From basics to depth

And

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *