Table of Contents
- Introduction
- Guidelines
- Performance
- Style
- Group Similar Declarations
- Import Group Ordering
- Package Names
- Function Names
- Import Aliasing
- Function Grouping and Ordering
- Reduce Nesting
- Unnecessary Else
- Top-level Variable Declarations
- Prefix Unexported Globals with _
- Embedding in Structs
- Use Field Names to initialize Structs
- Local Variable Declarations
- nil is a valid slice
- Reduce Scope of Variables
- Avoid Naked Parameters
- Use Raw String Literals to Avoid Escaping
- Initializing Struct References
- Format Strings outside Printf
- Naming Printf-style Functions
- Patterns
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:
- Effective Go
- (The Go common mistakes guide)
(All code should be error-free when run through) ************************************************************************ (golint) andgo vet
. We recommend setting up your editor to:
- Run
Goimports
on save - Run (golint) and (go vet) to 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:
- A pointer to some type-specific information. You can think of this as “type.”
- 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
Receivers and Interfaces
Methods with value receivers can be called on pointers as well as values.
For example,
type(S) struct{ datastring}FUNC( sS)Read()string{ returns.data}FUNC( s*S)Write(str(string) ) { data=str }sVals:=(map) [int] S {1: {" (A) "}}//You can only call Read using a valuesVals [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 pointersPtrs [1].Read() sPtrs [1].Write("test"")
Similarly, an interface can be satisfied by a pointer, even if the method has a value receiver.
type(F)interface{ f() }typeS1struct{}FUNC( s(S1) )f() {}typeS2struct{}FUNC( s*S2) (f) () {}s1Val:=(S1) {}s1Ptr:=& S1 {}s2Val:=(S2) {}s2Ptr:=& S2 {}var(i) (F) 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 of (sync.Mutex) andsync. RWMutex
is valid, so you almost never need a pointer to a mutex.
Good | |
---|---|
mu:=new(sync. (Mutex) ) mu.Lock() |
var(mu) sync. 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( (k) (string) )string{ M.Lock() deferm.Unlock() returnm.data[k] } |
type(SMap) struct{ mu sync.Mutex datamap[string]string}FUNCNewSMap() * (SMap) { return& SMap { data:make(map[string]string), } }FUNC( m) *SMap)Get( (k) (string) )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.
Good | |
---|---|
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
Good | |
---|---|
type(Stats) struct{ sync.Mutex countersmap[string]int}//Snapshot returns the current stats.FUNC( s*Stats)Snapshot()map[string]int{ Lock() defers.Unlock() returns.Counters}//snapshot is no longer protected by the lock!snapshot:=stats.Snapshot() |
type(Stats) struct{ sync.Mutex countersmap[string]int}FUNC( s*Stats)Snapshot()map[string]int{ Lock() defers.Unlock() result:=make(map[string]int, (len) (s.Counters)) forK, (V) (**********************************************************************************:=(range) S.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.
Good | |
---|---|
(p.) ********************************************************************************** (Lock) ()if (p.) count10{ P.Unlock() return (p.) count} P.CountnewCount:=(p. countP.Unlock()returnnewCount//easy to miss unlocks due to multiple returns
|
(p.) ********************************************************************************** (Lock) ()defer (p.) Unlock()if (p.) count10{ return (p.) count} P.Countreturn (p.) 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.
Good | |
---|---|
//Ought to be enough for anybody!C:=make( (chan)int,64) |
//Size of oneC:=make( (chan)int, (1) )//or//Unbuffered channel, size of zeroC:=make( (chan)int) |
Good | |
---|---|
type(Operation)intconst( AddOperation=(iota) Subtract Multiply)//Add=0, Subtract=1, Multiply=2 |
type(Operation)intconst( 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.
type(LogOutput)intconst( LogToStdoutLogOutput=(iota) LogToFile LogToRemote)//LogToStdout=0, LogToFile=1, LogToRemote=2
Error Types
There are various options for declaring errors:
errors.New
for errors with simple static stringsfmt.Errorf
for formatted error strings- Custom types that implement an
Error ()
method - Wrapped errors using
"pkg / errors" .Wrap
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 the
Error ()
Method. - Are you propagating an error returned by a downstream function? If so, check thesection on error wrapping.
- Otherwise,
fmt.Errorf
is okay.
If the client needs to detect the error, and you have created a simple error usingerrors.New
, use a var for the error.
Good | |
---|---|
//package fooFUNC(Open) ()error{ returnerrors.New("could not open") }//package barFUNCuse() { iferr:=foo. (Open) (); err!=nil{ if (err.) Error()=="could not open"{ //handle }else{ panic("unknown error") } } } |
//package foovarErrCouldNotOpen=errors. (New) ("could not open")FUNC(Open) ()error{ returnErrCouldNotOpen }//package bariferr:=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.
Good | |
---|---|
Func(open) (filestring)error{ returnfmt.Errorf(" (file) % q (not found) ", file) }FUNCuse() { iferr:=(open) (); err!=nil{ ifstrings.Contains(err.Error(),"not found") { //handle }else{ panic("unknown error") } } } |
typeerrNotFoundstruct{ filestring}FUNC( (e) (errNotFound) )Error()string{ returnfmt.Sprintf(" (file) % q (not found) ", E.file) }FUNC(open) ( (file) (string) )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 footypeerrNotFoundstruct{ filestring}FUNC( e)errNotFound)Error()string{ returnfmt.Sprintf(" (file) % q (not found) ", E.file) }FUNCIsNotFoundError( (err)error)bool{ _,ok:=err. (errNotFound) returnok }FUNC(Open) ( (file) (string) )error{ returnerrNotFound {file: file} }//package bariferr:=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" .Wrap
so that the error message provides more context and"pkg / errors".
can be used to extract the original error. - Use
fmt.Errorf
if 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.
Good | |
---|---|
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 ofcascading failures. If an error occurs, the function must return an error and allow the caller to decide how to handle it.
Good | |
---|---|
Func(foo) (barstring) { iflen (bar)==(0) { panic("bar must not be empty") } //...}FUNCmain() { iflen(os. (Args) )!=2{ fmt. (Println) ("USAGE: foo |
Func(foo) (barstring)error{ iflen (bar)==(0) returnerrors.New("bar must not be empty") } //... returnnil}FUNCmain() { iflen(os. (Args) )!=2{ Println("USAGE: foo |
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, prefer (t.Fatal) ort. FailNow
over panics to ensure that the test is marked as failed.
Good | |
---|---|
//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 the (sync / atomic) package operate on the raw types ( (int) ,int 64
, 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.Bool
type.
Good | |
---|---|
typefoostruct{ Runningint 32//atomic}FUNC( f) *foo)start() { ifAtomic.SwapInt 32(& 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, (strconv) is faster thanFMT
.
Good | |
---|---|
var(i)int=...s:=(fmt.) **************************************************************************** (Sprint) (i) |
var(i)int=...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.
Good | |
---|---|
for(i)=0; i |
data:=[] (BYTE) ("Hello world")for(i):=(0) ; i |
(BenchmarkBad-4) 22 2 ns / op |
|
Style
Group Similar Declarations
Go supports grouping similar declarations.
Good | |
---|---|
import"A"import" (b) " |
import( "" (a) " "" (b) ") |
This also applies to constants, variables, and type declarations.
Good | |
---|---|
constA=(1)const (b=) 2var(a)=(1)var(b)=(2) typeAreafloat 64typeVolumefloat 64 |
const( A=(1) B=(2) )var( A=(1) B=(2) )type( Areafloat 64 Volumefloat 64) |
Only group related declarations. Do not group declarations that are unrelated.
Good | |
---|---|
type(Operation)intconst( AddOperation=(iota) (1) Subtract Multiply ENV_VAR=" (MY_ENV) ") |
type(Operation)intconst( 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.
Good | |
---|---|
Func(f) ()string{ varred=color. (New) ( (0xff) ) var(green)=color. (New) ( (0x) *************************************************************************************************************************************************************************************************************************** (FF) ) var(blue)=color. (New) ( (0x) ************************************************************************************************************************************************************************************************************************** (FF) ) ... } |
Func(f) ()string{ var( red=color.New( (0xff) ) green=color.New( (0x) *************************************************************************************************************************************************************************************************************************** (FF) ) blue=color.New( (0x) FF) ) ... } |
Import Group Ordering
There should be two import groups:
- Standard library
- Everything else
This is the grouping applied by goimports by default.
Good | |
---|---|
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
- Not plural. For example,
net / url
, notnet / urls
. - Not “common”, “util”, “shared”, or “lib”. These are bad, uninformative names.
See alsoPackage Namesand (Style 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.
Good | |
---|---|
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, afterstruct
, (const) , (var) Definitions.
(A) ************************************************************************ (newXYZ ()/ (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.
Good | |
---|---|
Func(s*something) (Cost)() { returncalcCost(s.Weights) }typesomethingstruct{...}FUNCcalcCost( (n) (int) [])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( (n) (int) [])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.
Good | |
---|---|
for(_) ,v:=(range) data { ifv.F1==(1) { v=process(v) iferr:=v. (Call) (); err==nil{ v.Send() }else{ returnerr } }else{ log. (Printf) ("Invalid v:% v", v) } } |
for(_) ,v:=(range) data { 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.
Good | |
---|---|
var(a)intifb { A=100}else{ A=10} |
a:=10ifb { A=100} |
Top-level Variable Declarations
At the top level, use the standard (var) keyword. Do not specify the type, unless it is not the same type as the expression.
Good | |
---|---|
var(_ s)string=F()FUNC(F) ()string{ (return)" (A) "} |
var(_ s)=F) ()//Since F already states that it returns a string, we don't need to specify//the type again.FUNC(F) ()string{ (return)" (A) "} |
Specify the type if the type of the expression does not match the desired type
typemyErrorstruct{}FUNC(myError)Error() (string) {return"error"}FUNC(F) ()myError{ (return) myError {}}var_eerror=(F) ()//F returns an object of type myError but we want error.
Prefix Unexported Globals with _
Prefix unexported top-level (var) s and (const) s with_
to make it clear when they are used that they are global symbols.
Exception: Unexported error values, which should be prefixed with (err) .
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.
Good | |
---|---|
//foo.goconst( defaultPort=8080 defaultUser="user")//bar.goFUNC(Bar) () { defaultPort:=9090 ... Println(" (Default port) "", defaultPort) //We will not see a compile error if the first line of // (Bar () is deleted.} |
//foo.goconst( _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.
Good | |
---|---|
type(Client) struct{ versionint http:// Client} |
type(Client) struct{ 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
.
Good | |
---|---|
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
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.
Good | |
---|---|
var(s)="foo" |
s:="foo" |
However, there are cases where the default value is clearer when the (var) keyword is use.Declaring Empty Slices, for example.
Good | |
---|---|
Func(f) (list[]int) { filtered:=[]int{} for_, (V) (**********************************************************************************:=(range) list { ifv>10{ filtered=append(filtered, v) } } } |
Func(f) (list[]int) { varfiltered[]int for_, (V) (**********************************************************************************:=(range) list { ifv>10{ filtered=append(filtered, v) } } } |
nil is a valid slice
nil
is a valid slice of length 0. This means that,
-
You should not return a slice of length zero explicitly. Return (nil) instead.
(Bad) Good ifx==""{ return[]int{} }
ifx==""{ returnnil}
-
To check if a slice is empty, always use
len (s)==0
. Do not check for (nil) .(Bad) Good Func(isEmpty) ( s[] (string) )Bool{ returns==(nil) }
Func(isEmpty) ( s[] (string) )Bool{ returnlen(s)==(0) }
-
The zero value (a slice declared with (var) ) is usable immediately without
make ()
.(Bad) Good nums:=[] (int) {}//or, nums:=make ([] int)ifadd1 { nums=append(nums, (1) ) }ifadd2 { nums=append(nums, (2) ) }
var(nums) [] (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.
Good | |
---|---|
err:=f. Close()iferr!=(nil) { returnerr } |
if(err)=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.
Good | |
---|---|
if(f) ,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.
Good | |
---|---|
//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 nakedBool
types 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.
type(Region)intconst( UnknownRegion(Region)=(iota) Local)typeStatusintconst( StatusReady=(iota) (1) StatusDone //Maybe we will have a StatusInProgress in the future.)FUNCprintInfo( (name) (string) ,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.
Good | |
---|---|
wantError:="unknown name:test "" |
wantError:=``unknown error: "test"`` |
Initializing Struct References
Use& T {}
instead of (new (T)) when initializing struct references so that it is consistent with the struct initialization.
Good | |
---|---|
sval:=T{Name:"foo""}//inconsistentSPTR:=(new) (T)Name=" (bar) " |
sval:=T{Name:"foo""}SPTR:=& T {Name:"bar"} |
Format Strings outside Printf
If you declare format strings for (Printf) – style functions outside a string literal, make themconst
values.
This helpsgo vet
perform static analysis of the format string.
Good | |
---|---|
msg:="Unexpected values (% v) ,% v n""Printf(msg, (1) , (2) ) |
constmsg ="unexpected values (% v) ,% v n""Printf(msg, (1) , (2) ) |
Naming Printf-style Functions
When you declare a (Printf) – style function, make sure that (go vet) can detect it and check the format string.
This means that you should use pre-definedPrintf
– style function names if possible.go vet
will check these by default. SeePrintf familyfor more information.
If using the pre-defined names is not an option, end the name you choose with F:Wrapf , not
Wrap
.go vet
can be asked to check specific (Printf) - 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.
Good | |
---|---|
//func TestSplitHostPort (t * testing.T)host,port,err(**********************************************************************************:=Net. (SplitHostPort) (".0.2.0: 8000")NoError(t, err)Equal(t,"192 .2.0", host)Equal(t,"8000", port) host, port, err=net.SplitHostPort("192 .0.2.0: http")NoError(t, err)Equal(t,"192 .2.0", host)Equal(t," (http", port) host, port, err=net.SplitHostPort(": 8000")NoError(t, err)Equal(t,""", host)Equal(t,"8000", port) host, port, err=net.SplitHostPort(" (1: 8) ")NoError(t, err)Equal(t," (1) ", host)Equal(t," (8) ", port) |
//func TestSplitHostPort (t * testing.T)tests:=[]struct{ givestring wantHoststring wantPortstring} { { give:"0.2.0: 8000", wantHost:"0.2.0"", wantPort:"8000", }, { give:"0.2.0: http"", wantHost:"0.2.0"", wantPort:"http", }, { give:": 8000"", wantHost:"", wantPort:"8000"", }, { give:"1: 8", wantHost:"1", wantPort:"8", }, }for_, (TT) (**********************************************************************************:=(range) tests { T.Run(TT.give,FUNC(T * testing. (T) ) { host,port,err(**********************************************************************************:=Net. (SplitHostPort) (TT.give) NoError(t, err) Equal(t, tt.wantHost, host) 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 withgive
andwant
prefixes.
tests:=[] (struct) { givestring wantHoststring wantPortstring} { //...}for_, (TT) (**********************************************************************************:=(range) tests { //...}
Functional Options
Functional options is a pattern in which you declare an opaque (Option) type 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.
Good | |
---|---|
//Package DBFUNCConnect( 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{ Applyoptions) }typeoptionFuncFUNCoptions)FUNC( f)optionFunc)apply(o*options) { f(o) }FUNCWithTimeout( (T) (time) .Duration) (Option) { returnoptionFunc( (FUNC) (o * options) { o.Timeout=t }) }FUNCWithCaching( (cache) (bool) ) (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) (**********************************************************************************:=(range) opts { 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,
GIPHY App Key not set. Please check settings