func DeleteUser(
ctx context.Context,
name string,
+ softDelete bool,
) (...)
Abhinav Gupta
I’m Abhinav.
I work on Go frameworks.
Let’s talk about writing Go.
Rules
Why
Patterns
Practices
Exported means forever
Function types are fixed
Interfaces are immutable
Cannot remove
functions
variable
type
constants
once exported
Cannot modify
function
or
method
signatures
func DeleteUser(
ctx context.Context,
name string,
+ softDelete bool,
) (...)
adding a parameter
func DeleteUser(...) (
deleted bool,
+ uuid uuid.UUID,
err error,
)
or a return value
// ERROR
// - missing parameter: softDelete
// - cannot assign 3 values to 2 variables
deleted, err := DeleteUser(ctx, name)
if err != nil {
// ...
}
breaks existing callers
func ListPosts(
ctx context.Context,
+ skipAuthors ...string
) ([]*Post, error)
This is fine, right?
posts, err := ListPosts(ctx)
if err != nil {
// ...
}
Nope!
func Process(
ctx context.Context,
listPosts func(...) ([]*Post, error)
) {
posts, err := listPosts(ctx)
// ...
}
Breaks function references
Process(ctx, ListPosts)
// ERROR:
// want func(Context) (...)
// got func(Context, ...string) (...)
Cannot
modify
remove
add
methods
Adding methods to an interface
type Writer interface {
WriteBytes([]byte) error
+ WriteString(string) error
}
func WriteTo(w Writer) error {
// ...
}
breaks existing implementations
type myWriter struct{ /* ... */ }
func (w *myWriter) WriteBytes(
b []byte,
) error {
// ...
}
// ERROR:
// does not implement WriteString
err := WriteTo(&myWriter{...})
if err != nil {
// ...
}
Exported means forever
Function types are fixed
Interfaces are immutable
Be deliberate
You can always export it later
How to plan for expansion?
type DeleteUserRequest struct {
Name string
+ SoftDelete bool
}
func DeleteUser(
ctx context.Context,
req DeleteUserRequest,
) (...) {
// ...
}
struct
exclusively for parameters except Context
New optional fields
struct
. type DeleteUserResponse struct {
Deleted bool
+ UUID uuid.UUID
}
func DeleteUser(
...
) (DeleteUserResponse, error) {
// ...
}
struct
exclusively for returns except error
New fields for new outputs
struct
.type Option /* ... */
func SkipAuthors(
names ...string,
) Option { /* ... */ }
func ListPosts(
ctx context.Context,
opts ...Option,
) ([]*Post, error)
ListPosts(ctx)
ListPosts(ctx,
SkipAuthors(...),
)
type Option /* ... */
func SkipAuthors(
names ...string,
) Option { /* ... */ }
func Archived(
bool,
) Option { /* ... */ }
func ListPosts(
ctx context.Context,
opts ...Option,
) ([]*Post, error)
ListPosts(ctx)
ListPosts(ctx,
Archived(true),
SkipAuthors(...),
)
ListPosts(ctx,
Archived(false),
)
Do
Zero argument default
Required positional arguments
Don’t
Required options
Mix with parameter objects
type options struct {
}
type Option interface {
apply(*options)
}
type options struct {
skipAuthors []string
}
type Option interface {
apply(*options)
}
func SkipAuthors(
xs ...string,
) Option {
return skipAuthors(xs)
}
type skipAuthors []string
func (o skipAuthors) apply(...) {
opts.skipAuthors = []string(o)
}
type options struct {
skipAuthors []string
archived bool
postedAfter time.Time
}
type Option interface {
apply(*options)
}
func SkipAuthors(
xs ...string,
) Option { /* ... */ }
func Archived(
archived bool,
) Option { /* ... */ }
func PostedAfter(
postedAfter date.Date,
) Option { /* ... */ }
type options struct {
skipAuthors []string
archived bool
postedAfter time.Time
}
type Option interface {
apply(*options)
}
func ListPosts(
ctx context.Context,
os ...Option,
) (...) {
var opts options
for _, o := range os {
o.apply(&opts)
}
if opts.archived {
// ...
}
type options struct {
skipAuthors []string
archived bool
postedAfter time.Time
queryStats *Stats
}
type Stats struct {
Elapsed time.Duration
/* ... */
}
func QueryStats(*Stats) Option {
/* ... */
}
var stats Stats
posts, err := ListPosts(ctx,
QueryStats(&stats),
)
if err != nil {
// ...
}
log.Printf(
"Query took %v", stats.Elapsed)
// Output:
// Query took 100ms
Add new parameters or results
Compose options together
func(...Option) Option
High flexibility and high boilerplate
Parameter Objects
Result Objects
Functional Options
New Functions
How to plan for expansion?
Bad
type Writer interface {
WriteBytes([]byte) error
WriteString(string) error
}
func (w *myWriter) WriteString(
s string,
) error {
return w.WriteBytes([]byte(s))
}
Good
type Writer interface {
WriteBytes([]byte) error
}
func WriteString(
w Writer, s string,
) error {
return w.WriteBytes([]byte(s))
}
No helper methods — use functions
type Writer interface {
WriteBytes([]byte) error
}
func WriteString(w Writer, s string) error {
return w.WriteBytes([]byte(s))
}
type Writer interface {
WriteBytes([]byte) error
}
type StringWriter interface {
WriteString(string) error
}
func WriteString(w Writer, s string) error {
return w.WriteBytes([]byte(s))
}
type Writer interface {
WriteBytes([]byte) error
}
type StringWriter interface {
WriteString(string) error
}
func WriteString(w Writer, s string) error {
if sw, ok := w.(StringWriter); ok {
return sw.WriteString(s)
}
return w.WriteBytes([]byte(s))
}
type FS interface{
Open(string) (File, error)
}
func Stat(fs FS, name string) (FileInfo, error) {
f, err := fs.Open(name)
if err != nil {
return nil, err
}
return f.Stat()
}
type FS interface{
Open(string) (File, error)
}
type StatFS interface {
FS
Stat(string) (FileInfo, error)
}
func Stat(fs FS, name string) (FileInfo, error) {
if sf, ok := fs.(StatFS); ok {
return sf.Stat(name)
}
f, err := fs.Open(name)
if err != nil {
return nil, err
}
return f.Stat()
}
type FS interface{
Open(string) (File, error)
}
type ReadFileFS interface {
FS
ReadFile(string) ([]byte, error)
}
func ReadFile(fs FS, name string) ([]byte, error) {
if rf, ok := fs.(ReadFileFS); ok {
return rf.ReadFile(name)
}
f, err := fs.Open(name)
if err != nil {
return nil, err
}
return io.ReadAll(f)
}
DRY implementation
Upgrade as needed
No internal state
Wrapping breaks overrides
type Driver interface {
WriteBytes([]byte) error
}
type Writer struct{ drv Driver }
func (w *Writer) WriteBytes(bs []byte) error {
return w.drv.WriteBytes(bs)
}
type Writer struct{ drv Driver }
func (w *Writer) WriteBytes(bs []byte) error {
return w.drv.WriteBytes(bs)
}
func (w *Writer) WriteString(bs []byte) error {
return w.WriteBytes([]string(bs))
}
type StringWriter interface {
WriteString(string) error
}
type Writer struct{ drv Driver }
func (w *Writer) WriteBytes(bs []byte) error {
return w.drv.WriteBytes(bs)
}
func (w *Writer) WriteString(bs []byte) error {
if sw, ok := w.drv.(StringWriter); ok {
return sw.WriteString(s)
}
return w.WriteBytes([]string(bs))
}
Implement a small interface
Consume a large surface
Store internal state
zap.Logger
wraps zapcore.Core
http.Client
wraps http.RoundTripper
database/sql
wraps database/sql/driver
Lightweight concurrency
but not free
All goroutines
must finish or
must be stoppable
Bad
go func() {
for {
flush()
time.Sleep(delay)
}
}()
Good
stop := make(chan struct{})
go func() {
tick := time.NewTicker(delay)
defer tick.Stop()
for {
select {
case <-tick.C:
flush()
case <-stop:
return
}
}
}()
Stop with close(stop)
Bad
go func() {
for {
flush()
time.Sleep(delay)
}
}()
Better
ctx, cancel := /* ... */
go func() {
tick := time.NewTicker(delay)
defer tick.Stop()
for {
select {
case <-tick.C:
flush(ctx)
case <-ctx.Done():
return
}
}
}()
Stop with cancel()
Use a worker pool
func worker(
jobc <-chan job,
) {
for job := range jobc {
job.do()
}
}
close(jobc)
type manager struct {
jobc chan<- job
}
mgr := manager{
jobc: jobc,
}
for i := 0; i < NumWorkers; i++ {
go worker(jobc)
}
func (m *manager) Stop() {
close(m.jobc)
}
func worker(
ctx context.Context,
jobc <-chan job,
) {
for {
select {
case <-ctx.Done():
return
case job := <-jobc:
job.do(ctx)
}
}
}
close(jobc)
.type manager struct {
stop context.CancelFunc
jobc chan<- job
}
ctx, cancel := context.WithCancel(..)
mgr := manager{
stop: cancel,
jobc: jobc,
}
for i := 0; i < NumWorkers; i++ {
go worker(ctx, jobc)
}
func (m *manager) Stop() {
m.stop()
}
func worker(
ctx context.Context,
jobc <-chan job,
wg *sync.WaitGroup,
) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job := <-jobc:
job.do(ctx)
}
}
}
type manager struct {
stop context.CancelFunc
jobc chan<- job
wg sync.WaitGroup
}
ctx, cancel := context.WithCancel(..)
mgr := manager{
stop: cancel,
jobc: jobc,
}
mgr.wg.Add(NumWorkers)
for i := 0; i < NumWorkers; i++ {
go worker(ctx, jobc, &mgr.wg)
}
func (m *manager) Stop() {
m.stop()
m.wg.Wait()
}
Not a leaf error
out, err := f()
if err != nil {
return err
}
out, err := g()
if err != nil {
return fmt.Errorf("g: %v", err)
}
Leaf errors
return errors.New("great sadness")
return fmt.Errorf(
"unhappy result %q", result)
No
fmt.Errorf
/ errors.New
return errors.New("session is closed")
return fmt.Errorf(
"invalid email address %q", email)
Yes
Sentinel errors
Structured errors
Global error variables
package fs
var ErrNotExist =
errors.New("file does not exist")
var ErrExist =
errors.New("file already exists")
var ErrPermission =
errors.New("permission denied")
Prefix name with "Err"
Avoid overly verbose names (ErrFileAlreadyExists)
Match with errors.Is
if errors.Is(err, fs.ErrNotExist) {
createFile(..)
}
Error types
type SyntaxError struct {
Line, Column int
msg string
}
func (e *SyntaxError) Error() string
Suffix with "Error"
Use a pointer receiver
Match with errors.As
var synErr *SyntaxError
if errors.As(err, &synErr) {
highlight(synErr.Line)
}
Propagate:
return to caller
Handle:
react to it
out, err := f()
if err != nil {
return err
}
out, err := g()
if err != nil {
return fmt.Errorf("get g: %w")
}
%w
to retain error matchingtype FooError struct {
ID string
Err error
}
func (e *FooError) Unwrap() error {
return e.Err
}
out, err := h(id)
if err != nil {
return &FooError{ID: id, Err: err}
}
Unwrap()
to retain error matchingNo "failed to X"
fmt.Errorf("failed to load config: %w", err) // BAD
// failed to load config: failed to decode protobuf: ...
fmt.Errorf("load config: %w", err) // GOOD
// load config: decode protobuf: ...
Use %q
fmt.Errorf("get user %v: %w", user, err) // BAD
// get user : invalid name
fmt.Errorf("get user %q: %w", user, err) // GOOD
// get user "": invalid name
Don’t capitalize
fmt.Errorf("Send request: %w", err) // BAD
// Send request: Connect to "foo": ...
fmt.Errorf("send request: %w", err) // GOOD
// send request: connect to "foo": ...
sentinel errors ⇒ errors.Is
func loadConfig(..) (*Config, error) {
f, err := os.Open(name)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// Use default configuration
// if file does not exist.
return _defaultConfig, nil
}
// Fail on all other errors.
return nil, err
}
defer f.Close()
// ...
}
structured errors ⇒ errors.As
c, err := connect()
if err != nil {
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) && len(conns) {
// Fallback to a random existing
// connection, if any, if DNS resolution
// failed.
log.Warn(..., dnsErr.Name)
c = conns[rand.Intn(len(conns))]
} else {
// Fail on all other errors.
return nil, err
}
}
Use sentinel and structured errors when needed
fmt.Errorf
and errors.New
otherwise
Match with errors.Is
and errors.As
to handle
Don’t
export until necessary
fire-and-forget goroutines
noisy error messages
Do
plan for expansion
control goroutine lifetimes
expose and handle errors
Gopher image credits: egonelbre/gophers