Go essentials overview

File skeleton

package main            // one per file

import (
	"fmt"
	"os"

	"github.com/me/pkg" // own/3rd-party
)

const Name = "sdse"
type T struct{ N int }
func (t T) String() string { return t.N }
func main() { fmt.Println(os.Args) }

Hard rules: unused import or local var = compile error. Opening brace on the same line (semicolons are auto-inserted). One package per directory. No cyclic imports.

25 keywords

package importfile header
func var const typedeclare
struct interface map chancomposite types
if else switch case default fallthroughbranch
for range break continue gotoloop / jump
returnexit function
go select deferconcurrency / cleanup

Not keywords (shadowable, don't): true false nil iota any comparable error, type names, and the built-in functions.

Declare & assign

var x int           // zero value 0
var y = 42          // inferred
var z int = 42
w := 42             // func bodies ONLY

var (
	name  string
	tries = 3
)

a, b := 1, "two"    // multiple
a, err := f()       // := needs 1 new name
a, b = b, a         // swap: RHS evaluated first

Shadowing: err := ... inside an if/for body makes a new err that dies at the brace. Top cause of vanishing errors.

Zero values

0all numerics
falsebool
""string (never nil)
nilpointer slice map chan func interface
{...}struct/array: each element zeroed

Design so the zero value works: sync.Mutex, bytes.Buffer, strings.Builder all need no constructor.

const & iota

const Pi = 3.14159      // untyped: adapts
const S string = "hi"   // typed
// const t = time.Now() // ERROR: runtime value

type Level int
const (
	Debug Level = iota  // 0, resets per block
	Info                // 1 (expr repeats)
	Warn                // 2
)
const (
	_  = iota
	KB = 1 << (10 * iota) // 1024
	MB                    // 1048576
)

Only bool, string, numbers. Untyped consts carry arbitrary precision until used.

Types & conversion

int uint int8..64 uint8..64 uintptr
float32 float64 complex64 complex128
bool string
byte = uint8     rune = int32
any  = interface{}            // 1.18+
error                         // an INTERFACE
f := float64(i)      // ALWAYS explicit
n, err := strconv.Atoi("42")  // str<->num
var x int32; var y int64
z := int64(x) + y    // no mixed arithmetic

string(65) is "A", not "65" — use strconv.Itoa. uint8(300) wraps silently to 44.

Strings, bytes, runes

s := "Gø"
len(s)                    // 3 — BYTES not chars
s[0]                      // 71 — a byte
utf8.RuneCountInString(s) // 2 — chars
[]rune(s); []byte(s)      // allocates

for i, r := range s { }   // decodes UTF-8;
                          // i jumps by rune width

var b strings.Builder     // concat in a loop
b.WriteString("x")
b.String()

Immutable: s[0]='H' won't compile. += in a loop reallocates every pass.

Slices

var s []int            // nil, but append-able
s = append(s, 1, 2)    // ALWAYS reassign
t := []int{1,2,3,4,5}
u := make([]int, 0, 10)  // len 0, cap 10

t[1:3] // [2 3] half-open   t[:2]  t[2:]
copy(dst, src)  // min(len) elems
clear(s)        // zero all         (1.21+)
slices.Contains/Sort/Clone/Index     (1.21+)
view := orig[1:3]   // SHARES backing array
view[0] = 99        // orig changes too
view = append(view, 7) // writes through if
                       // cap allows, else
                       // reallocates+detaches

Aliasing depends on capacity → bugs are intermittent. Need independence? slices.Clone(v) or three-index v[1:3:3].

Maps

m := map[string]int{"a": 1}
n := make(map[string]int)
var p map[string]int  // nil: reads OK,
                      // WRITES PANIC

m["b"] = 2
v := m["nope"]        // 0, no error
v, ok := m["nope"]    // comma-ok
delete(m, "a")        // no-op if absent
len(m); clear(m)                    (1.21+)

for k, v := range m { }  // RANDOM ORDER
slices.Sorted(maps.Keys(m))         (1.23+)

Keys must be comparable (no slice/map/func). Set = map[K]struct{} (0 bytes per value).

Pointers

p := &x        // *int
*p = 20        // write through
q := new(int)  // zeroed, returns *int
var r *int     // nil: *r panics

func byVal(c Counter)  { c.n++ } // copy
func byPtr(c *Counter) { c.n++ } // original
byPtr(&c)
copiednum bool string array struct
header copied,
data shared
slice map chan func interface

No pointer arithmetic. Returning &Local{} is safe — escape analysis decides heap vs stack.

if / for

if v, err := f(); err != nil {   // init stmt
} else if x > 5 {
} else { }

for i := 0; i < 10; i++ { }  // C-style
for x < 100 { }              // while
for { }                      // infinite
for i, v := range xs { }
for k, v := range m { }      // random order
for i, r := range s { }      // string: runes
for v := range ch { }        // until closed
for i := range 10 { }        // 1.22+
for v := range seq { }       // 1.23+ iterator

outer:
for i := range g {
	for j := range g[i] {
		break outer      // or continue outer
	}
}

1.22+: loop vars are per-iteration, so closing over them in a goroutine is now safe.

switch

switch day {
case "sat", "sun":     // multiple values
	x()
case "mon":
	fallthrough        // explicit, rare
default:
}

switch {               // = if/else chain
case n >= 90: g = "A"
case n >= 80: g = "B"
}

switch v := x.(type) { // TYPE switch
case nil:
case int:    use(v * 2)   // v is int
case string: use(len(v))  // v is string
case error:  return v
default:     fmt.Printf("%T", v)
}

No implicit fallthrough. x must be an interface value in a type switch.

Functions

func add(a, b int) int { return a + b }

func div(a, b float64) (float64, error) {
	if b == 0 { return 0, errors.New("÷0") }
	return a / b, nil
}

func split(sum int) (x, y int) { // named
	x = sum / 2
	y = sum - x
	return                       // naked
}

func sum(nums ...int) int { }    // variadic
sum(1,2,3); sum(xs...)           // spread

double := func(x int) int { return x*2 }

func counter() func() int {      // closure
	n := 0
	return func() int { n++; return n }
}

Multiple returns are why Go needs no exceptions. Variadic param must be last.

defer

f, err := os.Open(p)
if err != nil { return err }
defer f.Close()      // runs on EVERY exit
mu.Lock()
defer mu.Unlock()
x := 1
defer fmt.Println(x) // prints 1: ARGS
x = 2                // EVALUATED NOW

for i := 0; i < 3; i++ {
	defer fmt.Print(i)  // LIFO -> 210
}

func t() (n int, err error) {
	defer func() {
		if err != nil { n = 0 } // can rewrite
	}()                         // named results
	return doWork()
}

Runs at function return, not block end — never defer in a long loop.

Type definitions

type Celsius float64   // NEW distinct type
type Temp = float64    // ALIAS, same type

func (c Celsius) String() string {
	return fmt.Sprintf("%.1f°C", float64(c))
}
c = Celsius(f)         // conversion needed
definitiondistinct; starts with no methods; you add your own
aliasinterchangeable; shares methods; can't add

A defined type keeps the underlying type's operators but inherits no methods. Any type can have methods — not just structs.

Structs

type Student struct {
	Name    string
	Email   string
	Courses []string
	active  bool        // unexported
}

s := Student{Name: "Ada"}   // KEYED always
p := &Student{Name: "Ada"}
p.Name                      // auto-deref, no ->

type Point struct{ X, Y int }
Point{1,2} == Point{1,2}    // true, field-wise
                            // (slice field = no ==)
type Empty struct{}         // 0 bytes
done := make(chan struct{})

Tags (read by reflection)

Name  string `json:"name" db:"full_name"`
Email string `json:"email,omitempty"`
Age   int    `json:"-"`     // never marshalled

Tag typos fail silently — run go vet.

Methods & receivers

func (c Counter) Value() int { return c.n }
func (c *Counter) Inc()      { c.n++ }

c.Inc()   // = (&c).Inc(), compiler helps
p.Value() // = (*p).Value()
pointermutates; large struct; holds a mutex
valuesmall/immutable; map, slice, basic type

Method sets: T has only value-receiver methods; *T has both. So *Counter satisfies an interface needing Inc() but Counter does not — interface satisfaction gets no auto-address help.

Be consistent: if one method needs a pointer, give them all pointers.

Factories

// no constructors; convention is NewT
func NewCounter(start int) *Counter {
	return &Counter{n: start}
}

func NewStudent(n string) (*Student, error) {
	if n == "" {
		return nil, errors.New("name required")
	}
	return &Student{Name: n}, nil
}
// one main type per pkg? call it New()

Interfaces

type Shape interface {
	Area() float64
	Perimeter() float64
}
type ReadWriter interface {  // embed
	io.Reader
	io.Writer
}

// satisfied IMPLICITLY — no "implements"
func (c Circle) Area() float64 { }
func (c Circle) Perimeter() float64 { }
var s Shape = Circle{R: 2}

var _ Shape = (*Circle)(nil) // compile check

Assertions

s := x.(string)      // panics if wrong
s, ok := x.(string)  // comma-ok
if sh, ok := x.(Shape); ok { }

Accept interfaces, return structs. Define them where consumed. Keep them tiny (io.Reader = 1 method). Don't write one until there are 2 implementations.

Nil trap: an interface is nil only if type and value are nil. Returning a nil *MyError as error makes err != nil TRUE.

Embedding

type Employee struct {
	Person          // no field name
	Company string
}
e := Employee{Person: Person{Name:"Ada"}}
e.Name            // promoted field
e.Greet()         // promoted method
e.Person.Greet()  // long form

func (e Employee) Greet() string {  // shadow
	return e.Person.Greet() + " @" + e.Company
}

type LoggingStore struct {
	Store           // embed INTERFACE:
	log *slog.Logger // wrap one method,
}                    // delegate the rest

Not polymorphism: a Person method calling Greet() always gets Person.Greet. No virtual dispatch to the outer type — use an interface for that.

Generics 1.18+

func Map[T, U any](xs []T, f func(T) U) []U {
	out := make([]U, 0, len(xs))
	for _, x := range xs { out = append(out, f(x)) }
	return out
}
Map(xs, func(s string) int { return len(s) })

type Number interface {
	~int | ~float64   // ~ = underlying type
}
func Sum[T Number](xs []T) T {
	var total T       // zero of T
	for _, x := range xs { total += x }
	return total
}

type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(v T) { }
st := &Stack[int]{}
anyanything
comparable== !=
cmp.Ordered< > (1.21+)

Check slices/maps first. Interface = behaviour varies; generic = same logic, different type.

Errors are values

type error interface { Error() string }

errors.New("not found")
fmt.Errorf("load %s: %v", p, err)  // no wrap
fmt.Errorf("load %s: %w", p, err)  // WRAPS

var ErrNotFound = errors.New("not found")

type ValidationError struct{ Field string }
func (e *ValidationError) Error() string { }
errors.Is(err, ErrNotFound)   // sentinel
var ve *ValidationError
errors.As(err, &ve)           // + binds ve
errors.Unwrap(err)            // one level
errors.Join(e1, e2)                 (1.20+)

Never err == ErrX (breaks on wrap) and never match on err.Error() text.

Lowercase, no trailing punctuation. Add context going up. Handle once: log or return, not both.

panic / recover

panic("unreachable")   // unwinds, runs defers

func safe() (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r)
		}
	}()                // ONLY works in a defer
	risky()
	return nil
}
nil derefindex out of range
bad type assertwrite to nil map
divide by zeroclose closed/nil chan

OK to panic: impossible states, MustCompile-style init, per-request recovery in a server.

recover does not cross goroutines — an unrecovered panic in any goroutine kills the process.

Goroutines & sync

go doWork()        // returns immediately
go func() { }()    // args eval'd at `go`
var wg sync.WaitGroup
for _, u := range urls {
	wg.Add(1)          // BEFORE go
	go func() {
		defer wg.Done()
		fetch(u)
	}()
}
wg.Wait()

wg.Go(func(){ fetch(u) })          // 1.25+
MutexLock/Unlock + defer
RWMutexmany readers, 1 writer
Onceonce.Do(f)
atomicInt64, CompareAndSwap
errgroupWaitGroup + first error

When main returns, every goroutine dies mid-flight. Nothing waits for you.

Run go test -race ./... — put it in CI.

Channels

ch  := make(chan int)      // UNBUFFERED
buf := make(chan int, 10)  // buffered
var nilCh chan int         // blocks forever

ch <- 42          // send
v := <-ch         // receive
v, ok := <-ch     // ok=false when drained
close(ch)         // SENDER closes, only once
for v := range ch { }   // until closed

func prod(out chan<- int) { }  // send-only
func cons(in  <-chan int) { }  // recv-only
opnilopenclosed
recvblocksblockszero,false
sendblocksblockspanic
closepanicokpanic

Don't communicate by sharing memory; share memory by communicating.

select & context

select {
case v := <-ch1:
case ch2 <- 42:
case <-time.After(2*time.Second):
case <-ctx.Done(): return ctx.Err()
default:            // makes it NON-BLOCKING
}

for {               // worker loop
	select {
	case job, ok := <-jobs:
		if !ok { return }
		handle(job)
	case <-ctx.Done():
		return
	}
}
ctx, cancel := context.WithCancel(parent)
defer cancel()                    // ALWAYS
ctx, cancel = context.WithTimeout(ctx, 5*time.Second)
func f(ctx context.Context, ...) // first param

Ready cases are picked at random. select{} blocks forever.

Patterns: worker pool · fan-out/fan-in · pipeline · done-channel.

Built-in functions

len caplength / capacity
make(T,n,c)init slice, map, chan → T
new(T)zeroed alloc → *T
appendgrow slice (use result!)
copy(d,s)min(len) elements
delete(m,k)remove key
clear(v)empty map / zero slice 1.21
min maxordered values 1.21
close(ch)finish a channel
panic recoverabort / resume
complex real imagcomplex numbers
print printlnstderr, unspecified — use fmt

make works on exactly 3 types (slice/map/chan) — the ones with runtime internals.

Special names

func main()              // pkg main, entry
func init()              // pre-main, any number
                         // per file, uncallable

go test

func TestXxx(t *testing.T)       // _test.go
func BenchmarkXxx(b *testing.B)  // b.N loop
func FuzzXxx(f *testing.F)
func ExampleXxx()   // checked if "// Output:"
func TestMain(m *testing.M)  // setup/teardown
t.Run/Parallel/Cleanup/Helper/Errorf

Stdlib interfaces

String()fmt.Stringer — used by %v/%s
Error()error (beats String)
Unwrap()feeds errors.Is/As
Read/Write/Closeio.Reader/Writer/Closer
ServeHTTPhttp.Handler
MarshalJSONjson.Marshaler

Conventions

New…factory
Must…panics instead of erroring
Err… / …Errorsentinel value / error type
…er1-method interface
GetX()never — it's just X()

Blank identifier

_, err := fmt.Println("x")   // discard
for _, v := range xs { }     // skip index
import _ "github.com/lib/pq" // init() only
var _ Shape = (*Circle)(nil) // assert

Packages & modules

type Student struct {
	Name  string   // exported
	email string   // package-private
}
// visibility = capital letter; the unit of
// encapsulation is the PACKAGE, not the type
import (
	"net/http"              // -> http
	mrand "math/rand"       // alias
	_ "github.com/lib/pq"   // side effects
)   // NO dot imports
cmd/<name>/one dir per binary
internal/import-blocked outside parent
testdata/ignored by the build
*_test.gotest-only compile
go mod init <path>   go mod tidy
go get pkg@v1.2.3    go work init ./a ./b

Names: short, lowercase, singular, no utils. The package name prefixes every call site.

go command

go run .compile + run
go build ./...compile all
go test ./...all tests
go test -racerace detector
go test -v -run TestXfilter by regex
go test -covercoverage
go test -bench=. -benchmembenchmarks
go vet ./...real-bug static checks
gofmt -l -w .canonical format
goimports -w .fmt + fix imports
go doc fmt.Printlndocs in terminal
GOOS=linux GOARCH=arm64 go build  // cross
CGO_ENABLED=0 go build   // static, FROM scratch
go build -ldflags="-X main.version=1.2.3"

CI gate: test -z "$(gofmt -l .)". One style, no options, no arguments.

fmt verbs

fmt.Println(a, b)        fmt.Printf("%s\n", s)
fmt.Sprintf(...)         fmt.Fprintf(w, ...)
fmt.Errorf("ctx: %w", err)
%v %+v %#vdefault · with field names · Go syntax
%Tdynamic type
%s %qstring · quoted
%d %b %o %xint base 10/2/8/16
%f %.2f %e %gfloat forms
%t %c %U %pbool · rune · U+00F8 · pointer
%wwrap (Errorf only)
%8.2f %-20s %08dwidth · left · zero-pad

%!d(string=hi) = wrong verb; go vet catches it at build time.

Gotchas

Dropping append's resultappend is lost → s = append(...)
Write to nil mappanic → make it
err shadowed by :=failure vanishes
Typed nil as errorerr != nil with no error
len(s) on non-ASCIIbytes ≠ chars
err == ErrXbreaks on wrap → errors.Is
Map orderrandomised on purpose
defer in a hot loophandles pile up
Sub-slice of a huge arraykeeps it all alive → Clone
string(65)"A" → strconv.Itoa
Unread channelgoroutine leak
Receiver closes a channelpanic on next send
for _, s := range xs + mutateit's a copy → xs[i].F
Value receiver + mutexcopies the lock (vet flags it)

Proverbs

  • Clear is better than clever.
  • Errors are values.
  • Don't just check errors, handle them gracefully.
  • Don't communicate by sharing memory; share memory by communicating.
  • The bigger the interface, the weaker the abstraction.
  • Make the zero value useful.
  • A little copying is better than a little dependency.
  • gofmt's style is no one's favourite, yet gofmt is everyone's favourite.