uber-go/guide


Table of Contents

Introduction

Styles are the conventions that govern our code. The term style is a bit of a
misnomer, since these conventions cover far more than just source file
formatting—gofmt handles that for us.

The goal of this guide is to manage this complexity by describing in detail the
Dos and Don’ts of writing Go code at Uber. These rules exist to keep the code
base manageable while still allowing engineers to use Go language features
productively.

This guide was originally created byPrashant VaranasiandSimon Newtonas
a way to bring some colleagues up to speed with using Go. Over the years it has
been amended based on feedback from others.

This documents idiomatic conventions in Go code that we follow at Uber. A lot
of these are general guidelines for Go, while others extend upon external
resources:

  1. Effective Go
  2. The Go common mistakes guide

All code should be error-free when run throughgolintandgo vet. We
recommend setting up your editor to:

  • Rungoimportson save
  • Rungolintandgo vetto check for errors

You can find information in editor support for Go tools here:
https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

Guidelines

Pointers to Interfaces

You almost never need a pointer to an interface. You should be passing
interfaces as values—the underlying data can still be a pointer.

An interface is two fields:

  1. A pointer to some type-specific information. You can think of this as
    “type.”
  2. Data pointer. If the data stored is a pointer, it’s stored directly. If
    the data stored is a value, then a pointer to the value is stored.

If you want interface methods to modify the underlying data, you must use a
pointer.

Receivers and Interfaces

Methods with value receivers can be called on pointers as well as values.

For example,

typeSstruct{
  datastring
}

func(sS)Read()string{
 returns.data
}

func(s*S)Write(strstring) {
  s.data=str
}

sVals:=map[int]S{1: {"A"}}

//You can only call Read using a value
sVals[1].Read()

//This will not compile:
// sVals[0].Write("test")

sPtrs:=map[int]*S{1: {"A"}}

//You can call both Read and Write using a pointer
sPtrs[1].Read()
sPtrs[1].Write("test")

Similarly, an interface can be satisfied by a pointer, even if the method has a
value receiver.

typeFinterface{
 f()
}

typeS1struct{}

func(sS1)f() {}

typeS2struct{}

func(s*S2)f() {}

s1Val:=S1{}
s1Ptr:=&S1{}
s2Val:=S2{}
s2Ptr:=&S2{}

variF
i=s1Val
i=s1Ptr
i=s2Ptr

//The following doesn't compile, since s2Val is a value, and there is no value receiver for f.
//  i=s2Val

Effective Go has a good write up onPointers vs. Values.

Zero-value Mutexes are Valid

The zero-value ofsync.Mutexandsync.RWMutexis valid, so you almost
never need a pointer to a mutex.

BadGood
mu:=new(sync.Mutex)
mu.Lock()
varmusync.Mutex
mu.Lock()

If you use a struct by pointer, then the mutex can be a non-pointer field or,
preferably, embedded directly into the struct.

typesmapstruct{
  sync.Mutex

  datamap[string]string
}

funcnewSMap() *smap{
 return&smap{
    data:make(map[string]string),
  }
}

func(m*smap)Get(kstring)string{
  m.Lock()
 deferm.Unlock()

 returnm.data[k]
}
typeSMapstruct{
  mu sync.Mutex

  datamap[string]string
}

funcNewSMap() *SMap{
 return&SMap{
    data:make(map[string]string),
  }
}

func(m*SMap)Get(kstring)string{
  m.mu.Lock()
 deferm.mu.Unlock()

 returnm.data[k]
}
Embed for private types or types that need to implement the Mutex interface.For exported types, use a private lock.

Copy Slices and Maps at Boundaries

Slices and maps contain pointers to the underlying data so be wary of scenarios
when they need to be copied.

Receiving Slices and Maps

Keep in mind that users can modify a map or slice you received as an argument
if you store a reference to it.

BadGood
func(d*Driver)SetTrips(trips[]Trip) {
  d.trips=trips
}

trips:=...
d1.SetTrips(trips)

//Did you mean to modify d1.trips?
trips[0]=...
func(d*Driver)SetTrips(trips[]Trip) {
  d.trips=make([]Trip,len(trips))
 copy(d.trips, trips)
}

trips:=...
d1.SetTrips(trips)

//We can now modify trips[0] without affecting d1.trips.
trips[0]=...

Returning Slices and Maps

Similarly, be wary of user modifications to maps or slices exposing internal
state.

BadGood
typeStatsstruct{
  sync.Mutex

  countersmap[string]int
}

//Snapshot returns the current stats.
func(s*Stats)Snapshot()map[string]int{
  s.Lock()
 defers.Unlock()

 returns.counters
}

//snapshot is no longer protected by the lock!
snapshot:=stats.Snapshot()
typeStatsstruct{
  sync.Mutex

  countersmap[string]int
}

func(s*Stats)Snapshot()map[string]int{
  s.Lock()
 defers.Unlock()

 result:=make(map[string]int,len(s.counters))
 fork,v:=ranges.counters{
    result[k]=v
  }
 returnresult
}

//Snapshot is now a copy.
snapshot:=stats.Snapshot()

Defer to Clean Up

Use defer to clean up resources such as files and locks.

BadGood
p.Lock()
ifp.count10{
  p.Unlock()
 returnp.count
}

p.count++
newCount:=p.count
p.Unlock()

returnnewCount

//easy to miss unlocks due to multiple returns
p.Lock()
deferp.Unlock()

ifp.count10{
 returnp.count
}

p.count++
returnp.count

//more readable

Defer has an extremely small overhead and should be avoided only if you can
prove that your function execution time is in the order of nanoseconds. The
readability win of using defers is worth the miniscule cost of using them. This
is especially true for larger methods that have more than simple memory
accesses, where the other computations are more significant than thedefer.

Channel Size is One or None

Channels should usually have a size of one or be unbuffered. By default,
channels are unbuffered and have a size of zero. Any other size
must be subject to a high level of scrutiny. Consider how the size is
determined, what prevents the channel from filling up under load and blocking
writers, and what happens when this occurs.

BadGood
//Ought to be enough for anybody!
c:=make(chanint,64)
//Size of one
c:=make(chanint,1)//or
//Unbuffered channel, size of zero
c:=make(chanint)

Start Enums at One

The standard way of introducing enumerations in Go is to declare a custom type
and aconstgroup withiota. Since variables have a 0 default value, you
should usually start your enums on a non-zero value.

BadGood
typeOperationint

const(
 AddOperation=iota
 Subtract
 Multiply
)

//Add=0, Subtract=1, Multiply=2
typeOperationint

const(
 AddOperation=iota+1
 Subtract
 Multiply
)

//Add=1, Subtract=2, Multiply=3

There are cases where using the zero value makes sense, for example when the
zero value case is the desirable default behavior.

typeLogOutputint

const(
 LogToStdoutLogOutput=iota
 LogToFile
 LogToRemote
)

//LogToStdout=0, LogToFile=1, LogToRemote=2

Error Types

There are various options for declaring errors:

When returning errors, consider the following to determine the best choice:

  • Is this a simple error that needs no extra information? If so,errors.New
    should suffice.
  • Do the clients need to detect and handle this error? If so, you should use a
    custom type, and implement theError()method.
  • Are you propagating an error returned by a downstream function? If so, check
    thesection on error wrapping.
  • Otherwise,fmt.Errorfis okay.

If the client needs to detect the error, and you have created a simple error
usingerrors.New, use a var for the error.

BadGood
//package foo

funcOpen()error{
 returnerrors.New("could not open")
}

//package bar

funcuse() {
 iferr:=foo.Open(); err !=nil{
   iferr.Error()=="could not open"{
     //handle
    }else{
     panic("unknown error")
    }
  }
}
//package foo

varErrCouldNotOpen=errors.New("could not open")

funcOpen()error{
 returnErrCouldNotOpen
}

//package bar

iferr:=foo.Open(); err !=nil{
 iferr==foo.ErrCouldNotOpen{
   //handle
  }else{
   panic("unknown error")
  }
}

If you have an error that clients may need to detect, and you would like to add
more information to it (e.g., it is not a static string), then you should use a
custom type.

BadGood
funcopen(filestring)error{
 returnfmt.Errorf("file%qnot found", file)
}

funcuse() {
 iferr:=open(); err !=nil{
   ifstrings.Contains(err.Error(),"not found") {
     //handle
    }else{
     panic("unknown error")
    }
  }
}
typeerrNotFoundstruct{
  filestring
}

func(eerrNotFound)Error()string{
 returnfmt.Sprintf("file%qnot found", e.file)
}

funcopen(filestring)error{
 returnerrNotFound{file: file}
}

funcuse() {
 iferr:=open(); err !=nil{
   if_,ok:=err.(errNotFound); ok {
     //handle
    }else{
     panic("unknown error")
    }
  }
}

Be careful with exporting custom error types directly since they become part of
the public API of the package. It is preferable to expose matcher functions to
check the error instead.

//package foo

typeerrNotFoundstruct{
  filestring
}

func(eerrNotFound)Error()string{
 returnfmt.Sprintf("file%qnot found", e.file)
}

funcIsNotFoundError(errerror)bool{
 _,ok:=err.(errNotFound)
 returnok
}

funcOpen(filestring)error{
 returnerrNotFound{file: file}
}

//package bar

iferr:=foo.Open("foo"); err !=nil{
 iffoo.IsNotFoundError(err) {
   //handle
  }else{
   panic("unknown error")
  }
}

Error Wrapping

There are three main options for propagating errors if a call fails:

  • Return the original error if there is no additional context to add and you
    want to maintain the original error type.
  • Add context using"pkg/errors".Wrapso that the error message provides
    more context and"pkg/errors".Causecan be used to extract the original
    error.
  • Usefmt.Errorfif the callers do not need to detect or handle that
    specific error case.

It is recommended to add context where possible so that instead of a vague
error such as “connection refused”, you get more useful errors such as “failed to
call service foo: connection refused”.

See alsoDon’t just check errors, handle them gracefully.

Handle Type Assertion Failures

The single return value form of atype assertionwill panic on an incorrect
type. Therefore, always use the “comma ok” idiom.

BadGood
t:=i.(string)
t,ok:=i.(string)
if!ok {
 //handle the error gracefully
}

Don’t Panic

Code running in production must avoid panics. Panics are a major source of
cascading failures. If an error occurs, the function must return an error and
allow the caller to decide how to handle it.

BadGood
funcfoo(barstring) {
 iflen(bar)==0{
   panic("bar must not be empty")
  }
 //...
}

funcmain() {
 iflen(os.Args) !=2{
    fmt.Println("USAGE: foo")
    os.Exit(1)
  }
 foo(os.Args[1])
}
funcfoo(barstring)error{
 iflen(bar)==0
   returnerrors.New("bar must not be empty")
  }
 //...
 returnnil
}

funcmain() {
 iflen(os.Args) !=2{
    fmt.Println("USAGE: foo")
    os.Exit(1)
  }
 iferr:=foo(os.Args[1]); err !=nil{
   panic(err)
  }
}

Panic/recover is not an error handling strategy. A program must panic only when
something irrecoverable happens such as a nil dereference. An exception to this is
program initialization: bad things at program startup that should abort the
program may cause panic.

var_statusTemplate=template.Must(template.New("name").Parse("_statusHTML"))

Even in tests, prefert.Fatalort.FailNowover panics to ensure that the
test is marked as failed.

BadGood
//func TestFoo(t *testing.T)

f,err:=ioutil.TempFile("","test")
iferr !=nil{
 panic("failed to set up test")
}
//func TestFoo(t *testing.T)

f,err:=ioutil.TempFile("","test")
iferr !=nil{
  t.Fatal("failed to set up test")
}

Use go.uber.org/atomic

Atomic operations with thesync/atomicpackage operate on the raw types
(int32,int64, etc.) so it is easy to forget to use the atomic operation to
read or modify the variables.

go.uber.org/atomicadds type safety to these operations by hiding the
underlying type. Additionally, it includes a convenientatomic.Booltype.

BadGood
typefoostruct{
  runningint32//atomic
}

func(f*foo)start() {
 ifatomic.SwapInt32(&f.running,1)==1{
    //already running…
    return
  }
 //start the Foo
}

func(f*foo)isRunning()bool{
 returnf.running==1//race!
}
typefoostruct{
  running atomic.Bool
}

func(f*foo)start() {
 iff.running.Swap(true) {
    //already running…
    return
  }
 //start the Foo
}

func(f*foo)isRunning()bool{
 returnf.running.Load()
}

Performance

Performance-specific guidelines apply only to the hot path.

Prefer strconv over fmt

When converting primitives to/from strings,strconvis faster than
fmt.

BadGood
variint=...
s:=fmt.Sprint(i)
variint=...
s:=strconv.Itoa(i)

Avoid string-to-byte conversion

Do not create byte slices from a fixed string repeatedly. Instead, perform the
conversion once and capture the result.

BadGood
fori:=0; iN; i++ {
  w.Write([]byte("Hello world"))
}
data:=[]byte("Hello world")
fori:=0; iN; i++ {
  w.Write(data)
}
BenchmarkBad-4   50000000   22.2 ns/op
BenchmarkGood-4  500000000   3.25 ns/op

Style

Group Similar Declarations

Go supports grouping similar declarations.

BadGood
import"a"
import"b"
import(
 "a"
 "b"
)

This also applies to constants, variables, and type declarations.

BadGood
consta=1
constb=2



vara=1
varb=2



typeAreafloat64
typeVolumefloat64
const(
  a=1
  b=2
)

var(
  a=1
  b=2
)

type(
 Areafloat64
 Volumefloat64
)

Only group related declarations. Do not group declarations that are unrelated.

BadGood
typeOperationint

const(
 AddOperation=iota+1
 Subtract
 Multiply
 ENV_VAR="MY_ENV"
)
typeOperationint

const(
 AddOperation=iota+1
 Subtract
 Multiply
)

constENV_VAR="MY_ENV"

Groups are not limited in where they can be used. For example, you can use them
inside of functions.

BadGood
funcf()string{
 varred=color.New(0xff0000)
 vargreen=color.New(0x00ff00)
 varblue=color.New(0x0000ff)

  ...
}
funcf()string{
 var(
    red  =color.New(0xff0000)
    green=color.New(0x00ff00)
    blue =color.New(0x0000ff)
  )

  ...
}

Import Group Ordering

There should be two import groups:

  • Standard library
  • Everything else

This is the grouping applied by goimports by default.

BadGood
import(
 "fmt"
 "os"
 "go.uber.org/atomic"
 "golang.org/x/sync/errgroup"
)
import(
 "fmt"
 "os"

 "go.uber.org/atomic"
 "golang.org/x/sync/errgroup"
)

Package Names

When naming packages, choose a name that is,

  • All lower-case. No capitals or underscores.
  • Does not need to be renamed using named imports at most call sites.
  • Short and succint. Remember that the name is identified in full at every call
    site.
  • Not plural. For example,net/url, notnet/urls.
  • Not “common”, “util”, “shared”, or “lib”. These are bad, uninformative names.

See alsoPackage NamesandStyle guideline for Go packages.

Function Names

We follow the Go community’s convention of usingMixedCaps for function
names
. An exception is made for test functions, which may contain underscores
for the purpose of grouping related test cases, e.g.,
TestMyFunction_WhatIsBeingTested.

Import Aliasing

Import aliasing must be used if the package name does not match the last
element of the import path.

import(
 "net/http"

  client"example.com/client-go"
  trace"example.com/trace/v2"
)

In all other scenarios, import aliases should be avoided unless there is a
direct conflict between imports.

BadGood
import(
 "fmt"
 "os"


  nettrace"golang.net/x/trace"
)
import(
 "fmt"
 "os"
 "runtime/trace"

  nettrace"golang.net/x/trace"
)

Function Grouping and Ordering

  • Functions should be sorted in rough call order.
  • Functions in a file should be grouped by receiver.

Therefore, exported functions should appear first in a file, after
struct,const,vardefinitions.

AnewXYZ()/NewXYZ()may appear after the type is defined, but before the
rest of the methods on the receiver.

Since functions are grouped by receiver, plain utility functions should appear
towards the end of the file.

BadGood
func(s*something)Cost() {
 returncalcCost(s.weights)
}

typesomethingstruct{ ... }

funccalcCost(nint[])int{...}

func(s*something)Stop() {...}

funcnewSomething() *something{
   return&something{}
}
typesomethingstruct{ ... }

funcnewSomething() *something{
   return&something{}
}

func(s*something)Cost() {
 returncalcCost(s.weights)
}

func(s*something)Stop() {...}

funccalcCost(nint[])int{...}

Reduce Nesting

Code should reduce nesting where possible by handling error cases/special
conditions first and returning early or continuing the loop. Reduce the amount
of code that is nested multiple levels.

BadGood
for_,v:=rangedata {
 ifv.F1==1{
    v=process(v)
   iferr:=v.Call(); err==nil{
      v.Send()
    }else{
     returnerr
    }
  }else{
    log.Printf("Invalid v:%v", v)
  }
}
for_,v:=rangedata {
 ifv.F1!=1{
    log.Printf("Invalid v:%v", v)
   continue
  }
  
  v=process(v)
 iferr:=v.Call(); err !=nil{
   returnerr
  }
  v.Send()
}

Unnecessary Else

If a variable is set in both branches of an if, it can be replaced with a
single if.

BadGood
varaint
ifb {
  a=100
}else{
  a=10
}
a:=10
ifb {
  a=100
}

Top-level Variable Declarations

At the top level, use the standardvarkeyword. Do not specify the type,
unless it is not the same type as the expression.

BadGood
var_sstring=F()

funcF()string{return"A"}
var_s=F()
//Since F already states that it returns a string, we don't need to specify
//the type again.

funcF()string{return"A"}

Specify the type if the type of the expression does not match the desired type
exactly.

typemyErrorstruct{}

func(myError)Error()string{return"error"}

funcF()myError{returnmyError{} }

var_eerror=F()
//F returns an object of type myError but we want error.

Prefix Unexported Globals with _

Prefix unexported top-levelvars andconsts with_to make it clear when
they are used that they are global symbols.

Exception: Unexported error values, which should be prefixed witherr.

Rationale: Top-level variables and constants have a package scope. Using a
generic name makes it easy to accidentally use the wrong value in a different
file.

BadGood
//foo.go

const(
  defaultPort=8080
  defaultUser="user"
)

//bar.go

funcBar() {
 defaultPort:=9090
  ...
  fmt.Println("Default port", defaultPort)

 //We will not see a compile error if the first line of
 //Bar() is deleted.
}
//foo.go

const(
  _defaultPort=8080
  _defaultUser="user"
)

Embedding in Structs

Embedded types (such as mutexes) should be at the top of the field list of a
struct, and there must be an empty line separating embedded fields from regular
fields.

BadGood
typeClientstruct{
  versionint
  http.Client
}
typeClientstruct{
  http.Client

  versionint
}

Use Field Names to initialize Structs

You should almost always specify field names when initializing structs. This is
now enforced bygo vet.

BadGood
k:=User{"John","Doe",true}
k:=User{
   FirstName:"John",
   LastName:"Doe",
   Admin:true,
}

Exception: Field namesmaybe omitted in test tables when there are 3 or
fewer fields.

tests:=[]struct{
}{
  opOperation
  wantstring
}{
  {Add,"add"},
  {Subtract,"subtract"},
}

Local Variable Declarations

Short variable declarations (:=) should be used if a variable is being set to
some value explicitly.

BadGood
vars="foo"
s:="foo"

However, there are cases where the default value is clearer when thevar
keyword is use.Declaring Empty Slices, for example.

BadGood
funcf(list[]int) {
 filtered:=[]int{}
 for_,v:=rangelist {
   ifv>10{
      filtered=append(filtered, v)
    }
  }
}
funcf(list[]int) {
 varfiltered[]int
 for_,v:=rangelist {
   ifv>10{
      filtered=append(filtered, v)
    }
  }
}

nil is a valid slice

nilis a valid slice of length 0. This means that,

  • You should not return a slice of length zero explicitly. Returnnil
    instead.

    BadGood
    ifx==""{
     return[]int{}
    }
    ifx==""{
     returnnil
    }
  • To check if a slice is empty, always uselen(s)==0. Do not check for
    nil.

    BadGood
    funcisEmpty(s[]string)bool{
     returns==nil
    }
    funcisEmpty(s[]string)bool{
     returnlen(s)==0
    }
  • The zero value (a slice declared withvar) is usable immediately without
    make().

    BadGood
    nums:=[]int{}
    //or, nums :=make([]int)
    
    ifadd1 {
      nums=append(nums,1)
    }
    
    ifadd2 {
      nums=append(nums,2)
    }
    varnums[]int
    
    ifadd1 {
      nums=append(nums,1)
    }
    
    ifadd2 {
      nums=append(nums,2)
    }

Reduce Scope of Variables

Where possible, reduce scope of variables. Do not reduce the scope if it
conflicts withReduce Nesting.

BadGood
err:=f.Close()
iferr !=nil{
returnerr
}
iferr:=f.Close(); err !=nil{
returnerr
}

If you need a result of a function call outside of the if, then you should not
try to reduce the scope.

BadGood
iff,err:=os.Open("f"); err==nil{
  _, err=io.WriteString(f,"data")
 iferr !=nil{
   returnerr
  }
 returnf.Close()
}else{
 returnerr
}
f,err:=os.Open("f")
iferr !=nil{
  returnerr
}

if_,err:=io.WriteString(f,"data"); err !=nil{
 returnerr
}

returnf.Close()

Avoid Naked Parameters

Naked parameters in function calls can hurt readability. Add C-style comments
(/* ... */) for parameter names when their meaning is not obvious.

BadGood
//func printInfo(name string, isLocal, done bool)

printInfo("foo",true,true)
//func printInfo(name string, isLocal, done bool)

printInfo("foo",true/*isLocal*/,true/*done*/)

Better yet, replace nakedbooltypes with custom types for more readable and
type-safe code. This allows more than just two states (true/false) for that
parameter in the future.

typeRegionint

const(
 UnknownRegionRegion=iota
 Local
)

typeStatusint

const(
 StatusReady=iota+1
 StatusDone
 //Maybe we will have a StatusInProgress in the future.
)

funcprintInfo(namestring,regionRegion,statusStatus)

Use Raw String Literals to Avoid Escaping

Go supportsraw string literals,
which can span multiple lines and include quotes. Use these to avoid
hand-escaped strings which are much harder to read.

BadGood
wantError:="unknown name:"test""
wantError:=`unknown error:"test"`

Initializing Struct References

Use&T{}instead ofnew(T)when initializing struct references so that it
is consistent with the struct initialization.

BadGood
sval:=T{Name:"foo"}

//inconsistent
sptr:=new(T)
sptr.Name="bar"
sval:=T{Name:"foo"}

sptr:=&T{Name:"bar"}

Format Strings outside Printf

If you declare format strings forPrintf-style functions outside a string
literal, make themconstvalues.

This helpsgo vetperform static analysis of the format string.

BadGood
msg:="unexpected values%v,%vn"
fmt.Printf(msg,1,2)
constmsg="unexpected values%v,%vn"
fmt.Printf(msg,1,2)

Naming Printf-style Functions

When you declare aPrintf-style function, make sure thatgo vetcan detect
it and check the format string.

This means that you should use pre-definedPrintf-style function
names if possible.go vetwill check these by default. SeePrintf family
for more information.

If using the pre-defined names is not an option, end the name you choose with
f:Wrapf, notWrap.go vetcan be asked to check specificPrintf-style
names but they must end with f.

$ go vet -printfuncs=wrapf,statusf

See alsogo vet: Printf family check.

Patterns

Test Tables

Use table-driven tests withsubteststo avoid duplicating code when the core
test logic is repetitive.

BadGood
//func TestSplitHostPort(t *testing.T)

host,port,err:=net.SplitHostPort("192.0.2.0:8000")
require.NoError(t, err)
assert.Equal(t,"192.0.2.0", host)
assert.Equal(t,"8000", port)

host, port, err=net.SplitHostPort("192.0.2.0:http")
require.NoError(t, err)
assert.Equal(t,"192.0.2.0", host)
assert.Equal(t,"http", port)

host, port, err=net.SplitHostPort(":8000")
require.NoError(t, err)
assert.Equal(t,"", host)
assert.Equal(t,"8000", port)

host, port, err=net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t,"1", host)
assert.Equal(t,"8", port)
//func TestSplitHostPort(t *testing.T)

tests:=[]struct{
  give    string
  wantHoststring
  wantPortstring
}{
  {
    give:    "192.0.2.0:8000",
    wantHost:"192.0.2.0",
    wantPort:"8000",
  },
  {
    give:    "192.0.2.0:http",
    wantHost:"192.0.2.0",
    wantPort:"http",
  },
  {
    give:    ":8000",
    wantHost:"",
    wantPort:"8000",
  },
  {
    give:    "1:8",
    wantHost:"1",
    wantPort:"8",
  },
}

for_,tt:=rangetests {
  t.Run(tt.give,func(t *testing.T) {
   host,port,err:=net.SplitHostPort(tt.give)
    require.NoError(t, err)
    assert.Equal(t, tt.wantHost, host)
    assert.Equal(t, tt.wantPort, port)
  })
}

Test tables make it easier to add context to error messages, reduce duplicate
logic, and add new test cases.

We follow the convention that the slice of structs is referred to astests
and each test casett. Further, we encourage explicating the input and output
values for each test case withgiveandwantprefixes.

tests:=[]struct{
  give    string
  wantHoststring
  wantPortstring
}{
 //...
}

for_,tt:=rangetests {
 //...
}

Functional Options

Functional options is a pattern in which you declare an opaqueOptiontype
that records information in some internal struct. You accept a variadic number
of these options and act upon the full information recorded by the options on
the internal struct.

Use this pattern for optional arguments in constructors and other public APIs
that you foresee needing to expand, especially if you already have three or
more arguments on those functions.

BadGood
//package db

funcConnect(
  addrstring,
  timeout time.Duration,
  cachingbool,
) (*Connection,error) {
 //...
}

//Timeout and caching must always be provided,
//even if the user wants to use the default.

db.Connect(addr, db.DefaultTimeout, db.DefaultCaching)
db.Connect(addr, newTimeout, db.DefaultCaching)
db.Connect(addr, db.DefaultTimeout,false/*caching*/)
db.Connect(addr, newTimeout,false/*caching*/)
typeoptionsstruct{
  timeout time.Duration
  cachingbool
}

//Option overrides behavior of Connect.
typeOptioninterface{
 apply(*options)
}

typeoptionFuncfunc(*options)

func(foptionFunc)apply(o*options) {
 f(o)
}

funcWithTimeout(ttime.Duration)Option{
 returnoptionFunc(func(o *options) {
    o.timeout=t
  })
}

funcWithCaching(cachebool)Option{
 returnoptionFunc(func(o *options) {
    o.caching=cache
  })
}

//Connect creates a connection.
funcConnect(
  addrstring,
  opts ...Option,
) (*Connection,error) {
 options:=options{
    timeout: defaultTimeout,
    caching: defaultCaching,
  }

 for_,o:=rangeopts {
    o.apply(&options)
  }

 //...
}

//Options must be provided only if needed.

db.Connect(addr)
db.Connect(addr, db.WithTimeout(newTimeout))
db.Connect(addr, db.WithCaching(false))
db.Connect(
  addr,
  db.WithCaching(false),
  db.WithTimeout(newTimeout),
)

See also,

Read More

LEAVE A REPLY

Please enter your comment!
Please enter your name here