DEV Community

Cover image for Google's Go Style Guide: The Rules Worth Adopting on Any Team
Gabriel Anhaia
Gabriel Anhaia

Posted on

Google's Go Style Guide: The Rules Worth Adopting on Any Team


You open a pull request. Someone drops a link to
Google's Go Style Guide
in the comments and asks you to "follow the decisions doc." The
review stalls while two engineers argue about whether an empty slice
should be nil or []T{}, and whether the struct you just wrote has
its fields in the wrong order.

The guide is worth reading. It is three documents: the Style Guide
(the short list of principles), Style Decisions (the long list of
specific calls), and Best Practices (patterns). It codifies how Go
gets written inside a company with a very large monorepo and a lot of
reviewers who need to agree.

That last part matters. Some of the rules exist because Google has
thousands of engineers touching shared code. Your team of six does not
have Google's constraints, so you can take the good rules and leave the
ones that only pay off at that scale. Here are four the doc is specific
about, and where each lands.

Adopt: prefer nil slices over empty ones

The guide says to prefer nil slices. A nil slice and an empty
[]T{} slice behave the same for the operations you use every day.

var a []int          // nil slice
b := []int{}         // empty, non-nil slice

fmt.Println(len(a), len(b)) // 0 0
a = append(a, 1)     // works fine on nil
b = append(b, 1)     // works fine too

for range a { }      // ranges zero times, no panic
Enter fullscreen mode Exit fullscreen mode

len, append, and range all treat a nil slice as a
zero-length slice. So the default var results []Item is a working
empty slice with no extra allocation. You do not need make or []T{}
to "initialize" it before appending.

Adopt this one. It removes a whole class of pointless initialization
and reads cleaner. One caveat the guide flags: encoding/json treats
them differently.

type Resp struct {
    Items []string `json:"items"`
}

var nilResp Resp
nilResp.Items = nil
// marshals to: {"items":null}

emptyResp := Resp{Items: []string{}}
// marshals to: {"items":[]}
Enter fullscreen mode Exit fullscreen mode

If a JSON client expects [] and not null, that difference is real
and can break a frontend. That is the one place to reach for []T{}
on purpose. Everywhere else, let it be nil.

Adopt: wrap errors with %w only when the caller needs the cause

The guide's error-wrapping rule is more precise than "always use
%w." The decision is about whether the caller should be able to
inspect the underlying error.

Use %w when you want callers to reach the wrapped error through
errors.Is or errors.As:

func loadConfig(path string) (*Config, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("loadConfig %q: %w", path, err)
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Now a caller can write errors.Is(err, os.ErrNotExist) and it works,
because the chain is preserved.

Use %v when you deliberately do not want to expose the cause,
because the underlying error is an implementation detail you might
change later:

func (s *Store) Save(u User) error {
    if err := s.db.Insert(u); err != nil {
        return fmt.Errorf("save user %d: %v", u.ID, err)
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Here the SQL driver's error type is not part of your API. Wrapping
with %w would let callers depend on it, and swapping the driver
later would break them. %v flattens it to a string and keeps your
contract narrow.

Adopt this one. The distinction between "I'm exposing this cause on
purpose" and "this cause is internal" is a design decision, and the
guide is right to make you choose per call site instead of reaching
for %w on autopilot.

Adopt: name tests and failures so you can read the output

The guide is opinionated about test output, and it is correct. A test
failure should tell you what was tested, what you got, and what you
wanted, without opening the source file.

Table tests get named cases, and the subtest name comes from the case:

func TestParse(t *testing.T) {
    tests := []struct {
        name    string
        in      string
        want    int
        wantErr bool
    }{
        {name: "single digit", in: "7", want: 7},
        {name: "empty input", in: "", wantErr: true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Parse(tt.in)
            if (err != nil) != tt.wantErr {
                t.Fatalf("Parse(%q) err = %v, wantErr %v",
                    tt.in, err, tt.wantErr)
            }
            if got != tt.want {
                t.Errorf("Parse(%q) = %d, want %d",
                    tt.in, got, tt.want)
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Two things the guide pushes here. Name the case field name and pass
it to t.Run, so a failure prints TestParse/empty_input and you know
which row broke. And write failure messages as
FuncName(args) = got, want X, so the message reads like the assertion
it failed. When this test breaks in CI, the log line is enough to
locate the bug.

Adopt all of it. It costs nothing at write time and saves you the
round trip of reopening the test to understand a red build.

Skip (mostly): reordering struct fields for alignment

Here is the rule that gets misapplied. There is a vet analyzer,
fieldalignment, that reorders struct fields to shrink memory padding.
Someone reads about it, runs it across the codebase, and now your
structs are sorted by byte size instead of by meaning.

// Grouped by what the fields mean:
type Server struct {
    Host string
    Port int

    ReadTimeout  time.Duration
    WriteTimeout time.Duration

    TLS     bool
    Verbose bool
}
Enter fullscreen mode Exit fullscreen mode

Now the "optimally" packed version, with the same fields
sorted by byte size instead of by meaning:

type ServerPacked struct {
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
    Host         string
    Port         int
    TLS          bool
    Verbose      bool
}
Enter fullscreen mode Exit fullscreen mode

The second version might save a few bytes per struct. For a type you
allocate a handful of times, that saving is nothing, and you paid for
it by breaking the logical grouping that helps a reader.

The guide's own principle is that clarity beats micro-optimization.
Field order should follow meaning. Reordering for padding is worth it
only when you allocate the struct in enormous quantities and have
measured that the padding matters. That is a real case for some code,
and a distraction for most of it.

So skip the blanket fieldalignment pass. Keep it in your back pocket
for the rare hot struct where a profiler pointed you at it. Do not let
a linter sort your fields by default.

How to bring this to a team

The guide is a menu, not a mandate. Pull the rules that hold at any
scale into your review checklist: nil slices by default, %w versus
%v as a conscious choice, test names and got/want messages. Leave
the scale-specific ones, like reflexive field reordering, out of the
default flow.

Write the short version down in your repo. Three or four lines that
say "we prefer nil slices, we wrap with %w only to expose a cause, our
test failures print got/want." That is a house style the whole team can
hold in their head, which is the point of a style guide in the first
place.


If this was useful

Style rules like these are downstream of understanding how Go actually
behaves. Nil slices make sense once you know the slice header, and the
%w decision makes sense once you know how the error chain is walked.
The Complete Guide to Go Programming covers the language and runtime
underneath these rules. Hexagonal Architecture in Go shows how to
keep error wrapping honest across the boundary between your domain and
the framework. That boundary is where the %w versus %v call
actually earns its keep.

Thinking in Go — the 2-book series on Go programming and hexagonal architecture

Top comments (0)