跳到正文
awesomego.org

Naming

Function and method names

Avoid repetition

When choosing the name for a function or method, consider the context in which the name will be read. Consider the following recommendations to avoid excess repetition at the call site:

  • The following can generally be omitted from function and method names:

    • The types of the inputs and outputs (when there is no collision)
    • The type of a method’s receiver
    • Whether an input or output is a pointer
  • For functions, do not repeat the name of the package.

    // Bad:
    package yamlconfig
    
    func ParseYAMLConfig(input string) (*Config, error)
    // Good:
    package yamlconfig
    
    func Parse(input string) (*Config, error)
  • For methods, do not repeat the name of the method receiver.

    // Bad:
    func (c *Config) WriteConfigTo(w io.Writer) (int64, error)
    // Good:
    func (c *Config) WriteTo(w io.Writer) (int64, error)
  • Do not repeat the names of variables passed as parameters.

    // Bad:
    func OverrideFirstWithSecond(dest, source *Config) error
    // Good:
    func Override(dest, source *Config) error
  • Do not repeat the names and types of the return values.

    // Bad:
    func TransformToJSON(input *Config) *jsonconfig.Config
    // Good:
    func Transform(input *Config) *jsonconfig.Config

When it is necessary to disambiguate functions of a similar name, it is acceptable to include extra information.

// Good:
func (c *Config) WriteTextTo(w io.Writer) (int64, error)
func (c *Config) WriteBinaryTo(w io.Writer) (int64, error)

Naming conventions

There are some other common conventions when choosing names for functions and methods:

  • Functions that return something are given noun-like names.

    // Good:
    func (c *Config) JobName(key string) (value string, ok bool)

    A corollary of this is that function and method names should avoid the prefix Get.

    // Bad:
    func (c *Config) GetJobName(key string) (value string, ok bool)
  • Functions that do something are given verb-like names.

    // Good:
    func (c *Config) WriteDetail(w io.Writer) (int64, error)
  • Identical functions that differ only by the types involved include the name of the type at the end of the name.

    // Good:
    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 clear “primary” version, the type can be omitted from the name for that version:

    // Good:
    func (c *Config) Marshal() ([]byte, error)
    func (c *Config) MarshalText() (string, error)

Test double and helper packages

There are several disciplines you can apply to naming packages and types that provide test helpers and especially test doubles. A test double could be a stub, fake, mock, or spy.

These examples mostly use stubs. Update your names accordingly if your code uses fakes or another kind of test double.

Suppose you have a well-focused package providing production code similar to this:

package creditcard

import (
    "errors"

    "path/to/money"
)

// ErrDeclined indicates that the issuer declines the charge.
var ErrDeclined = errors.New("creditcard: declined")

// Card contains information about a credit card, such as its issuer,
// expiration, and limit.
type Card struct {
    // omitted
}

// Service allows you to perform operations with credit cards against external
// payment processor vendors like charge, authorize, reimburse, and subscribe.
type Service struct {
    // omitted
}

func (s *Service) Charge(c *Card, amount money.Money) error { /* omitted */ }

Creating test helper packages

Suppose you want to create a package that contains test doubles for another. We’ll use package creditcard (from above) for this example:

One approach is to introduce a new Go package based on the production one for testing. A safe choice is to append the word test to the original package name (“creditcard” + “test”):

// Good:
package creditcardtest

Unless stated explicitly otherwise, all examples in the sections below are in package creditcardtest.

Simple case

You want to add a set of test doubles for Service. Because Card is effectively a dumb data type, similar to a Protocol Buffer message, it needs no special treatment in tests, so no double is required. If you anticipate only test doubles for one type (like Service), you can take a concise approach to naming the doubles:

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

// Stub stubs creditcard.Service and provides no behavior of its own.
type Stub struct{}

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

This is strictly preferable to a naming choice like StubService or the very poor StubCreditCardService, because the base package name and its domain types imply what creditcardtest.Stub is.

Finally, if the package is built with Bazel, make sure the new go_library rule for the package is marked as testonly:

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

The approach above is conventional and will be reasonably well understood by other engineers.

See also:

Multiple test double behaviors

When one kind of stub is not enough (for example, you also need one that always fails), we recommend naming the stubs according to the behavior they emulate. Here we rename Stub to AlwaysCharges and introduce a new stub called AlwaysDeclines:

// Good:
// AlwaysCharges stubs creditcard.Service and simulates success.
type AlwaysCharges struct{}

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

// AlwaysDeclines stubs creditcard.Service and simulates declined charges.
type AlwaysDeclines struct{}

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

Multiple doubles for multiple types

But now suppose that package creditcard contains multiple types worth creating doubles for, as seen below with Service and StoredValue:

package creditcard

type Service struct {
    // omitted
}

type Card struct {
    // omitted
}

// StoredValue manages customer credit balances.  This applies when returned
// merchandise is credited to a customer's local account instead of processed
// by the credit issuer.  For this reason, it is implemented as a separate
// service.
type StoredValue struct {
    // omitted
}

func (s *StoredValue) Credit(c *Card, amount money.Money) error { /* omitted */ }

In this case, more explicit test double naming is sensible:

// Good:
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

When variables in your tests refer to doubles, choose a name that most clearly differentiates the double from other production types based on context. Consider some production code you want to test:

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

In the tests, a test double called a “spy” for CreditCard is juxtaposed against production types, so prefixing the name may improve clarity.

// Good:
package payment

import "path/to/creditcardtest"

func TestProcessor(t *testing.T) {
    var spyCC creditcardtest.Spy
    proc := &Processor{CC: spyCC}

    // declarations omitted: card and amount
    if err := proc.Process(card, amount); err != nil {
        t.Errorf("proc.Process(card, amount) = %v, want nil", err)
    }

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

This is clearer than when the name is not prefixed.

// Bad:
package payment

import "path/to/creditcardtest"

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

    proc := &Processor{CC: cc}

    // declarations omitted: card and amount
    if err := proc.Process(card, amount); err != nil {
        t.Errorf("proc.Process(card, amount) = %v, want nil", err)
    }

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

Shadowing

Note: This explanation uses two informal terms, stomping and shadowing. They are not official concepts in the Go language spec.

Like many programming languages, Go has mutable variables: assigning to a variable changes its value.

// Good:
func abs(i int) int {
    if i < 0 {
        i *= -1
    }
    return i
}

When using short variable declarations with the := operator, in some cases a new variable is not created. We can call this stomping. It’s OK to do this when the original value is no longer needed.

// Good:
// innerHandler is a helper for some request handler, which itself issues
// requests to other backends.
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(ctx, "Capped deadline in inner request")

    // Code here no longer has access to the original context.
    // This is good style if when first writing this, you anticipate
    // that even as the code grows, no operation legitimately should
    // use the (possibly unbounded) original context that the caller provided.

    // ...
}

Be careful using short variable declarations in a new scope, though: that introduces a new variable. We can call this shadowing the original variable. Code after the end of the block refers to the original. Here is a buggy attempt to shorten the deadline conditionally:

// Bad:
func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse {
    // Attempt to conditionally cap the deadline.
    if *shortenDeadlines {
        ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
        defer cancel()
        ctxlog.Info(ctx, "Capped deadline in inner request")
    }

    // BUG: "ctx" here again means the context that the caller provided.
    // The above buggy code compiled because both ctx and cancel
    // were used inside the if statement.

    // ...
}

A correct version of the code might be:

// Good:
func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse {
    if *shortenDeadlines {
        var cancel func()
        // Note the use of simple assignment, = and not :=.
        ctx, cancel = context.WithTimeout(ctx, 3*time.Second)
        defer cancel()
        ctxlog.Info(ctx, "Capped deadline in inner request")
    }
    // ...
}

In the case we called stomping, because there’s no new variable, the type being assigned must match that of the original variable. With shadowing, an entirely new entity is introduced so it can have a different type. Intentional shadowing can be a useful practice, but you can always use a new name if it improves clarity.

It is not a good idea to use variables with the same name as standard packages other than very small scopes, because that renders free functions and values from that package inaccessible. Conversely, when picking a name for your package, avoid names that are likely to require import renaming or cause shadowing of otherwise good variable names at the client side.

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

Util packages

Go packages have a name specified on the package declaration, separate from the import path. The package name matters more for readability than the path.

Go package names should be related to what the package provides. Naming a package just util, helper, common or similar is usually a poor choice (it can be used as part of the name though). Uninformative names make the code harder to read, and if used too broadly they are liable to cause needless import conflicts.

Instead, consider what the callsite will look like.

// Good:
db := spannertest.NewDatabaseFromFile(...)

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

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

You can tell roughly what each of these do even without knowing the imports list (cloud.google.com/go/spanner/spannertest, io, and crypto/elliptic). With less focused names, these might read:

// Bad:
db := test.NewDatabaseFromFile(...)

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

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