Cheatsheet Go
Linguagem de programação do Google
Go
Basic Syntax
Variables
package main
import "fmt"
func main() {
var name string = "Anna"
var age int = 30
var active bool = true
var x, y int = 1, 2
fmt.Println(name, age, active, x, y)
}Declare variables with var, giving the type or letting Go infer it. You can declare several on the same line. The initial value, if omitted, is the type's zero value.
Operators
// Arithmetic + - * / % // Comparison == != > < >= <= // Logical && || ! // Bitwise & | ^ << >> // Address and dereference &x *p
Go has arithmetic, comparison, logical (&&, ||, !) and bitwise operators. % gives the division remainder. & gets a variable's address and * dereferences a pointer.
Type conversions
var f float64 = 3.99 var i int = int(f) // 3 (truncates) var n int = 65 var c string = string(n) // "A" (rune) // No implicit conversion var x int = 10 // var y float64 = x // ERROR var y float64 = float64(x) // OK
Go doesn't do automatic conversions between types: you must convert explicitly with int(), float64(), etc. Converting float64 to int truncates the decimal part.
Inference with :=
func main() {
price := 9.99 // float64
name := "Anna" // string
counter := 0 // int
ok := true // bool
// several at once
a, b := 1, 2
}The := operator declares and infers the type from the value, but only works inside functions. It's the most common way to create local variables. At package level use var.
Zero values
var i int // 0 var f float64 // 0.0 var s string // "" var b bool // false var p *int // nil var sl []int // nil var m map[string]int // nil
Every variable in Go has an automatic initial value, the zero value: 0 for numbers, an empty string for string, false for bool and nil for pointers, slices and maps. There are no uninitialized variables.
strconv
import "strconv"
// int -> string
s := strconv.Itoa(42) // "42"
// string -> int
n, err := strconv.Atoi("123") // 123
// others
strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
strconv.ParseFloat("3.14", 64)
strconv.FormatBool(true) // "true"
strconv.ParseBool("true")The strconv package converts between strings and basic types. Itoa() turns an int into a string and Atoi() does the reverse, also returning an error if the text is not valid.
Data types
// Integers int, int8, int16, int32, int64 uint, uint8, uint16, uint32, uint64 // Floating-point numbers float32, float64 // Others string // text (immutable) bool // true / false byte // alias for uint8 rune // alias for int32 (Unicode) complex64, complex128
Go is strongly typed. Integers have fixed sizes (int8 to int64); int depends on the architecture. float64 is the default for decimals and rune represents a Unicode character.
Basic strings
s := "Hello World" len(s) // length in bytes s[0] // first byte s[0:3] // substring "Hello" // Concatenate name := "Hello, " + "Anna" // Raw string (multi-line) text := `line 1 line 2`
Strings are immutable sequences of bytes. len() returns the size in bytes and s[0:3] extracts a part. Backticks create raw strings that accept multiple lines with no escapes.
Output with fmt
import "fmt"
fmt.Println("Hello") // with newline
fmt.Print("without ", "newline")
fmt.Printf("Name: %s\n", "Anna")
// Common verbs
fmt.Printf("%d\n", 42) // integer
fmt.Printf("%.2f\n", 3.14159) // 3.14
fmt.Printf("%t\n", true) // bool
fmt.Printf("%v\n", object) // default value
fmt.Printf("%+v\n", struct1) // with fields
fmt.Printf("%T\n", 3.14) // type
fmt.Sprintf("x=%d", 5) // returns a stringThe fmt package prints and formats. Println() adds a line break and Printf() uses verbs: %d integer, %s string, %f float, %v default value. Sprintf() returns the formatted string.
Constants and iota
const Pi = 3.14159
const (
StatusOK = 200
StatusNotFound = 404
)
// iota: automatic counter
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
)Constants are declared with const and cannot change. Inside a block, iota generates automatic sequential numbers (0, 1, 2...), ideal for enumerations.
strings package
import "strings"
s := "Hello World Go"
strings.Contains(s, "World") // true
strings.HasPrefix(s, "Hello") // true
strings.HasSuffix(s, "Go") // true
strings.Split(s, " ") // [Hello World Go]
strings.Join([]string{"a", "b"}, "-") // "a-b"
strings.Replace(s, "o", "0", -1)
strings.ToUpper(s)
strings.ToLower(s)
strings.Trim(" x ", " ") // "x"The strings package brings functions to manipulate text: Contains() searches, Split() divides and Join() joins. Replace() with -1 replaces all occurrences.
Flow Control
if / else
age := 20
if age >= 18 {
fmt.Println("adult")
} else if age > 12 {
fmt.Println("young")
} else {
fmt.Println("child")
}The if in Go doesn't use parentheses around the condition, but the braces are mandatory. You can chain else if and else.
for the while and infinite
// while
x := 10
for x > 0 {
x--
}
// infinite loop
for {
if condition {
break
}
}Go has no while: you use for with only the condition. A for with nothing creates an infinite loop, which ends with break or return.
defer
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close() // runs at the end
// several defers: LIFO order
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// output: 3, 2, 1defer postpones a call to the end of the function, ideal for cleanup (closing files and connections). Several defers run in reverse order (LIFO).
if with initialization
if err := doWork(); err != nil {
log.Fatal(err)
}
// err doesn't exist here
if n, ok := m["key"]; ok {
fmt.Println(n)
}You can declare a variable before the condition, separated by ;. It's scoped to the if block. It's the idiomatic pattern for checking errors and results.
break and continue
for i := 0; i < 10; i++ {
if i == 3 {
continue // skips 3
}
if i == 7 {
break // stops at 7
}
fmt.Println(i)
}
// break with a label (nested loops)
Loop:
for _, a := range x {
for _, b := range a {
if b == target {
break Loop
}
}
}continue jumps to the next iteration and break ends the loop. In nested loops, a label (like Loop:) lets break exit the outer loop.
Classic for
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// countdown counter
for i := 10; i > 0; i-- {
fmt.Println(i)
}The for is the only loop in Go. In its classic form it has three parts: initialization, condition and post-statement, separated by ;. It doesn't use parentheses.
switch
day := 2
switch day {
case 1:
fmt.Println("Monday")
case 2, 3:
fmt.Println("Tuesday or Wednesday")
case 4, 5:
fmt.Println("Thursday or Friday")
default:
fmt.Println("Weekend")
}The switch compares a value against several cases. In Go, each case ends automatically (no break). You can group several values in the same case, separated by commas.
for range
// slice
for i, v := range slice {
fmt.Println(i, v)
}
// map
for k, v := range map {
fmt.Println(k, v)
}
// string (runes)
for i, r := range "Hello" {
fmt.Println(i, string(r))
}
// ignore the index
for _, v := range slice { }range iterates over slices, maps, strings and channels, returning index and value. Use _ to ignore the index. On strings, it iterates by rune (Unicode character).
switch without condition
hour := 14
switch {
case hour < 12:
fmt.Println("Good morning")
case hour < 20:
fmt.Println("Good afternoon")
default:
fmt.Println("Good evening")
}
// fallthrough (rare)
switch x {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two")
}A switch with no expression works like a chain of if/else if, evaluating each condition in order. fallthrough forces execution of the next case (rarely used).
Functions
Simple function
func add(a, b int) int {
return a + b
}
// parameters of the same type
func average(a, b, c float64) float64 {
return (a + b + c) / 3
}
result := add(3, 4) // 7A function is declared with func, followed by the name, parameters and return type. Parameters of the same type can share the type (a, b int). The return type comes at the end.
Functions the values
var fn func(int) int
fn = func(x int) int {
return x * 2
}
fmt.Println(fn(5)) // 10
// in maps
operations := map[string]func(int, int) int{
"sum": func(a, b int) int { return a + b },
}Functions are first-class values in Go: you can store them in variables, maps or slices. The type func(int) int describes a function that takes and returns an int.
Recursion
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
func fibonacci(n int) int {
if n < 2 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
fmt.Println(factorial(5)) // 120A recursive function calls itself until it reaches a base case. factorial() multiplies until it reaches 1. Recursion is elegant, but for simple cases a loop can be more efficient.
Multiple returns
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}Go allows returning several values. The idiomatic pattern is to return (result, error), where error is nil when everything works. The caller always checks the error.
Anonymous functions
// run immediately
func() {
fmt.Println("executed already")
}()
// with an argument
func(name string) {
fmt.Println("Hello,", name)
}("Anna")
// assign to a variable
double := func(x int) int { return x * 2 }
fmt.Println(double(4)) // 8An anonymous function has no name and can be defined and called in the same place. It's widely used in go func() {...}() for goroutines and the an argument to other functions.
Named returns
func calculate(a, b int) (sum, product int) {
sum = a + b
product = a * b
return // returns sum and product
}
s, p := calculate(3, 4) // 7, 12Return values can be named, serving the local variables. A return with no values returns the current named values. Useful for documenting the meaning of each return.
Closures
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
inc := counter()
fmt.Println(inc()) // 1
fmt.Println(inc()) // 2
fmt.Println(inc()) // 3A closure is a function that captures variables from the environment where it was created. Here, the anonymous function keeps the state of n between successive calls.
Variadic functions
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6
sum(1, 2, 3, 4, 5) // 15
// expand a slice
vals := []int{1, 2, 3}
sum(vals...)Parameters with ... accept a variable number of arguments, received the a slice. You can pass an existing slice by expanding it with vals.... fmt.Println() is variadic.
Higher-order functions
// receive a function the a parameter
func apply(nums []int, fn func(int) int) []int {
res := make([]int, len(nums))
for i, v := range nums {
res[i] = fn(v)
}
return res
}
double := apply([]int{1, 2, 3}, func(x int) int {
return x * 2
}) // [2, 4, 6]Higher-order functions take or return other functions. Passing a function the a parameter lets you generalize behavior, like transforming each element of a slice.
Structs e Interfaces
Struct
type Person struct {
Name string
Age int
Active bool
}
// instantiate
p := Person{
Name: "Anna",
Age: 30,
Active: true,
}
fmt.Println(p.Name) // AnnaA struct groups fields of different types into a single type. It's created with type and instantiated by naming the fields. It's the basis for modeling data in Go.
Constructors
type User struct {
Name string
Email string
}
// convention: New + type name
func NewUser(name, email string) *User {
return &User{
Name: name,
Email: email,
}
}
u := NewUser("Anna", "ana@mail.com")Go has no native constructors; the convention is a NewX() function that returns a pointer to the already-initialized struct. It lets you validate data and set default values.
Type assertion
var i any = "hello"
// assert the type
s := i.(string)
fmt.Println(s)
// safe form (comma-ok)
s, ok := i.(string)
if ok {
fmt.Println("is string:", s)
}
n, ok := i.(int) // ok == falseA type assertion (i.(string)) extracts the concrete value from an interface. The safe form with ok avoids a panic if the type doesn't match.
Ways to instantiate
type Point struct{ X, Y int }
// with field names (recommended)
p1 := Point{X: 1, Y: 2}
// by position (field order)
p2 := Point{1, 2}
// zero value
var p3 Point // {0, 0}
// with new (returns a pointer)
p4 := new(Point)
p4.X = 5You can instantiate a struct with the field names (clearer) or by position. var p Point creates the zero value and new() returns a pointer to a zeroed struct.
Interfaces
type Shape interface {
Area() float64
Perimeter() float64
}
type Square struct{ Side float64 }
func (q Square) Area() float64 {
return q.Side * q.Side
}
func (q Square) Perimeter() float64 {
return 4 * q.Side
}
// Square implements Shape implicitlyAn interface defines a set of methods. In Go, implementation is implicit: any type with those methods satisfies the interface, with no explicit declaration. This favors decoupling.
Type switch
func describe(i any) {
switch v := i.(type) {
case int:
fmt.Println("integer:", v)
case string:
fmt.Println("text:", v)
case bool:
fmt.Println("bool:", v)
case []int:
fmt.Println("slice de int")
default:
fmt.Printf("other: %T\n", v)
}
}A type switch (i.(type)) tests the concrete type of an interface across several cases. The variable v takes the correct type in each branch. %T shows the type.
Methods (value receiver)
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
c := Circle{Radius: 5}
fmt.Println(c.Area())A method is a function with a receiver, declared between func and the name. With a value receiver (c Circle), the method works on a copy and doesn't change the original.
Empty interface (any)
// any is an alias for interface{}
func print(v any) {
fmt.Println(v)
}
print(42)
print("text")
print([]int{1, 2, 3})
var x any = "hello"The empty interface any (alias for interface{}) accepts any type, the it requires no methods. It's useful for generic functions, but loses type safety — use it sparingly.
Methods (pointer receiver)
type Counter struct {
Value int
}
func (c *Counter) Increment() {
c.Value++ // changes the original
}
ct := Counter{}
ct.Increment()
ct.Increment()
fmt.Println(ct.Value) // 2With a pointer receiver (c *Counter), the method can modify the original and avoids copying large structs. Use a pointer whenever you need to change the state.
Embedding (composition)
type Animal struct {
Name string
}
func (a Animal) Describe() string {
return "I am " + a.Name
}
type Dog struct {
Animal // embedded field
Raca string
}
c := Dog{Animal: Animal{Name: "Rex"}, Raca: "Labrador"}
fmt.Println(c.Name) // promoted
fmt.Println(c.Describe()) // promoted methodGo uses composition instead of inheritance. Embedding a type (with no field name) promotes its fields and methods to the outer type. Dog gains Name and Describe() from Animal.
Collections
Arrays
var arr [5]int // [0,0,0,0,0]
arr[0] = 10
nums := [3]int{1, 2, 3}
// inferred size
vals := [...]int{1, 2, 3, 4}
len(nums) // 3
// the size is part of the type
var a [3]int
var b [4]int
// a = b // ERROR: different typesAn array has a fixed size that is part of the type ([3]int differs from [4]int). It's passed by copy. In practice, you almost always use a slice, which is dynamic.
make and copy
// make: creates a slice with len and cap
s := make([]int, 5) // len=5, cap=5
s = make([]int, 0, 10) // len=0, cap=10
// copy: copies elements
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // n=3
// pre-sized map
m := make(map[string]int, 100)make() creates slices, maps and channels with an initial size, avoiding reallocations. copy(dst, src) copies elements between slices safely, without sharing the array.
Sorting (sort)
import "sort"
nums := []int{3, 1, 4, 1, 5}
sort.Ints(nums) // [1,1,3,4,5]
sort.Strings([]string{"b", "a", "c"})
// custom sorting
people := []Person{{"Anna", 30}, {"Ray", 25}}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
sort.SearchInts(nums, 3) // binary searchThe sort package sorts slices: Ints() and Strings() for basic types, sort.Slice() with a comparison function for custom types. SearchInts() does a binary search.
Slices
s := []int{1, 2, 3}
// empty slice
var empty []int // nil
other := []int{} // empty, not nil
len(s) // 3 (length)
cap(s) // capacity
s[0] // 1
s[2] // 3A slice is a dynamic view over an array, with a length and a capacity. It's the most used collection in Go. It's declared with []type with no fixed size.
Maps
m := map[string]int{
"apple": 3,
"banana": 5,
}
// empty map
empty := make(map[string]int)
m["orange"] = 2 // add
m["apple"] = 10 // update
fmt.Println(m["banana"]) // 5
len(m)A map stores key-value pairs. It's created with map[key]value{} or make(). Access and assignment use the m[key] syntax. Keys must be comparable.
slices package (Go 1.21+)
import "slices"
s := []int{3, 1, 2}
slices.Sort(s) // sorts
slices.Contains(s, 2) // true
slices.Index(s, 2) // position
slices.Reverse(s) // reverses
slices.Min(s) // minimum
slices.Max(s) // maximum
slices.Equal(a, b) // compares
slices.Clone(s) // independent copyThe slices package (Go 1.21+) brings modern generic functions: Contains(), Sort(), Min(), Max() and Clone(). Clone() creates a copy that doesn't share the array.
append
s := []int{1, 2, 3}
s = append(s, 4) // [1,2,3,4]
s = append(s, 5, 6) // several
s = append(s, []int{7, 8}...) // another slice
// important: reassign!
s = append(s, 9)append() adds elements to the end of a slice and returns the new slice — you must reassign (s = append(s, ...)). If the capacity reaches the limit, Go automatically allocates a larger array.
Maps: delete and comma-ok
m := map[string]int{"a": 1, "b": 2}
// comma-ok: does the key exist?
v, ok := m["a"]
if ok {
fmt.Println("exists:", v)
}
// zero value if it doesn't exist
v = m["z"] // 0 (no error!)
delete(m, "a") // removeAccessing a non-existent key returns the zero value with no error. That's why the comma-ok pattern (v, ok := m[key]) is used to know whether the key exists. delete() removes an entry.
Slicing
s := []int{0, 1, 2, 3, 4, 5}
s[1:4] // [1,2,3]
s[:3] // [0,1,2]
s[3:] // [3,4,5]
s[:] // full view
s[1:4:4] // low:high:max (limits cap)
// shares the underlying array!
sub := s[1:3]
sub[0] = 99 // changes s tooThe s[low:high] syntax creates a sub-slice. Careful: the sub-slice shares the original array, so changes affect both. Use copy() or the three-index form to isolate.
Iterating collections
// slice
for i, v := range []int{10, 20, 30} {
fmt.Println(i, v)
}
// map (random order!)
for k, v := range m {
fmt.Println(k, v)
}
// keys only
for k := range m {
fmt.Println(k)
}range iterates over slices (index, value) and maps (key, value). The iteration order of a map is intentionally random. Use _ to ignore the index or the key.
Competition
Goroutines
func task(n int) {
fmt.Println("task", n)
}
func main() {
go task(1) // goroutine
go func() {
fmt.Println("asynchronous")
}()
time.Sleep(time.Second) // simple wait
}A goroutine is a function run concurrently, launched with go. They are extremely lightweight (a few KB). The program ends when main() finishes, so you must synchronize.
Closing and ranging over channels
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // close when done
}()
// range ends when the channel closes
for v := range ch {
fmt.Println(v)
}
// comma-ok: channel closed?
v, ok := <-chclose(ch) signals there will be no more sends. A for range over the channel ends automatically when it closes. The v, ok := <-ch pattern returns ok == false if it's closed and empty.
atomic and RWMutex
import "sync/atomic" var total int64 atomic.AddInt64(&total, 1) // atomic increment v := atomic.LoadInt64(&total) // RWMutex: many reads, 1 write var rw sync.RWMutex rw.RLock() // read // ... read data ... rw.RUnlock() rw.Lock() // write // ... write ... rw.Unlock()
sync/atomic does atomic operations without locks, ideal for simple counters. sync.RWMutex allows several simultaneous reads (RLock()) but only one write (Lock()).
Channels
ch := make(chan string)
go func() {
ch <- "hello" // sends (blocks)
}()
msg := <-ch // receives (blocks)
fmt.Println(msg)A channel communicates between goroutines safely. ch <- value sends and <-ch receives. An unbuffered channel blocks until a receiver and a sender are both ready.
select
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case msg := <-ch2:
fmt.Println("ch2:", msg)
case <-time.After(time.Second):
fmt.Println("timeout")
default:
fmt.Println("none ready")
}select waits on several channel operations, running the first ready case. If several are ready, it picks one at random. default avoids blocking and time.After() creates timeouts.
Context
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)
defer cancel()
select {
case res := <-doWork(ctx):
fmt.Println(res)
case <-ctx.Done():
fmt.Println("cancelado:", ctx.Err())
}The context package controls cancellation and timeouts between goroutines. WithTimeout() creates a context that expires; ctx.Done() is a channel that closes on cancellation. Pass ctx the the first parameter.
Buffered channel
ch := make(chan int, 3) // buffer of 3 ch <- 1 // doesn't block ch <- 2 ch <- 3 // ch <- 4 // BLOCKS (full) fmt.Println(<-ch) // 1 (FIFO) fmt.Println(len(ch), cap(ch)) // 2 3
A buffered channel (make(chan int, 3)) accepts sends without blocking while there is space. It only blocks when it's full (send) or empty (receive).
WaitGroup
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait() // blocks until all finish
fmt.Println("done")sync.WaitGroup waits for a set of goroutines to finish. Add(1) registers each one, Done() (in defer) marks completion and Wait() blocks until they all end.
Directional channels
// send only
func produtor(ch chan<- int) {
ch <- 42
}
// receive only
func consumer(ch <-chan int) {
fmt.Println(<-ch)
}
ch := make(chan int)
go produtor(ch)
consumer(ch)You can restrict a channel's direction in the type: chan<- int sends only, <-chan int receives only. This makes APIs clearer and safer, indicating who produces and who consumes.
Mutex
var (
mu sync.Mutex
counter int
)
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
// 1000 safe goroutines
for i := 0; i < 1000; i++ {
go increment()
}A sync.Mutex protects shared data: Lock() blocks access and Unlock() releases it. Use defer mu.Unlock() to guarantee release even on error.
Error Handling
The error pattern
f, err := os.Open("data.txt")
if err != nil {
return fmt.Errorf("open file: %w", err)
}
defer f.Close()
// use fIn Go, errors are values. Functions that can fail return an error the the last return value. The idiomatic pattern is to check if err != nil immediately and propagate the error.
errors.Is
import (
"errors"
"the"
)
_, err := os.Open("nonexistent.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file not exists")
}
// compares along the wrapping chainerrors.Is(err, target) checks whether an error (or any error wrapped in the chain) matches a specific value, like os.ErrNotExist. It replaces direct comparison, which doesn't work with wrapping.
Creating errors
import (
"errors"
"fmt"
)
// simple error
err := errors.New("something failed")
// formatted error
err = fmt.Errorf("value %d is invalid", n)
return errerrors.New() creates an error with a fixed message. fmt.Errorf() creates formatted errors, like Sprintf. Both return a value that implements the error interface.
errors.As
var nf *NotFoundError
if errors.As(err, &nf) {
// nf now has the concrete type
fmt.Println("missing:", nf.Name)
}
// extracts the first error of the given typeerrors.As(err, &target) searches the chain for the first error of a concrete type and copies it into target. It lets you access specific fields of custom errors even after wrapping.
Wrapping with %w
func lerConfig() error {
_, err := os.Open("config.yaml")
if err != nil {
return fmt.Errorf("read config: %w", err)
}
return nil
}
// %w wraps the original error,
// preserving the chain of causesThe %w verb in fmt.Errorf() wraps the original error, creating a chain. This lets you inspect the cause later with errors.Is() and errors.As(), without losing context.
errors.Join (Go 1.20+)
import "errors"
err1 := validarNome(name)
err2 := validarEmail(email)
// combine several errors
err := errors.Join(err1, err2)
if err != nil {
return err // shows both
}
errors.Is(err, err1) // trueerrors.Join() (Go 1.20+) combines several errors into one, useful for validating multiple fields. The original errors remain detectable with errors.Is() and errors.As().
Custom error
type NotFoundError struct {
Name string
}
func (e *NotFoundError) Error() string {
return e.Name + " not found"
}
// use
return &NotFoundError{Name: "user"}Any type with an Error() string method implements the error interface. Custom errors can carry additional data (like Name), useful for the caller.
panic and recover
func safe() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("fatal error")
}
// panic interrupts; recover only in deferpanic() interrupts the flow and propagates up the stack; recover(), called inside a defer, catches it and regains control. It's a last resort — in Go, prefer returning an error.
Packages and Modules
go.mod
module github.com/user/project
go 1.22
require (
github.com/gorilla/mux v1.8.1
)The go.mod file defines the module: the path (name) and the Go version. It's created with go mod init. Dependencies go in require and the exact versions in go.sum.
go get
# add dependency go get github.com/gorilla/mux # specific version go get github.com/gorilla/mux@v1.8.1 # update go get -u ./... # remove unused go mod tidy
go get downloads and adds dependencies to go.mod. You can pin a version with @v1.8.1 and update everything with -u. go mod tidy cleans up the ones no longer used.
import
import (
"fmt"
"the"
"net/http"
alias "other/package" // rename
_ "driver/mysql" // side-effects only
. "math" // dot import (avoid)
)
fmt.Println("hello")import brings packages into the file. You can give an alias to avoid conflicts, use _ to run only the initialization (drivers) or . to import without a prefix (discouraged).
go mod
# create module go mod init github.com/user/proj # clean dependencies go mod tidy # download all go mod download # verify integrity go mod verify # show the dependency graph go mod graph
The go mod commands manage the module: init creates the go.mod, tidy syncs dependencies with the code, download stores them in the cache and verify confirms the checksums.
Exporting (uppercase)
package calculation
// Uppercase = exported (public)
func Add(a, b int) int {
return a + b
}
type Server struct {
Host string // public field
port int // private field
}
// lowercase = private to the package
func internal() { }In Go, visibility is by capitalization: names with an uppercase letter (Add, Server) are exported and accessible outside the package. Lowercase names are private to the package.
Project layout
meuprojeto/
├── go.mod
├── go.sum
├── main.go // package main
├── cmd/
│ └── server/
│ └── main.go // another binary
├── internal/ // private to the module
│ └── handler/
└── pkg/ // reusable
└── util/A Go project is organized by packages in folders. cmd/ holds the binaries, internal/ code private to the module (the compiler prevents external import) and pkg/ reusable code.
init()
package config
var baseURL string
func init() {
// runs before main()
baseURL = os.Getenv("API_URL")
if baseURL == "" {
baseURL = "http://localhost"
}
}The init() function runs automatically before main(), once per package. It's used to initialize state, register drivers or validate configuration. There can be several per file.
package main
package main
import "fmt"
// mandatory entry point
func main() {
fmt.Println("Hello, world!")
}An executable program needs package main and a main() function with no parameters or return value. It's the entry point. Library packages use other names.
Tips and CLI
CLI: essential commands
go run main.go # compiles and runs go build # builds a binary go install # compiles and installs go test ./... # runs tests go fmt ./... # formats code go mod tidy # cleans dependencies go clean # cleans caches
Go has a single tool, go. run compiles and runs, build builds the binary, test runs the tests and fmt formats code in the standard style. ./... covers all packages.
Useful interfaces
// fmt.Stringer
type Point struct{ X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d,%d)", p.X, p.Y)
}
// io.Reader / io.Writer
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}The standard library defines small, ubiquitous interfaces. fmt.Stringer (String()) customizes printing. io.Reader and io.Writer abstract any source or destination of data.
Best practices
// 1. Always handle errors
if err != nil {
return err
}
// 2. Short, clear names
i, v := range s // not "index, value"
// 3. Composition instead of inheritance
// 4. Short receiver
func (s *Server) Start() { }
// 5. Context the the 1st parameter
func Get(ctx context.Context, id string) { }Go values simplicity: always handle errors, use short names, prefer composition over inheritance and pass context.Context the the first parameter. Always format with go fmt.
Tests (testing)
// sum_test.go
package calculation
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2,3) = %d; quero %d", got, want)
}
}Tests live in _test.go files with TestX(t *testing.T) functions. Run them with go test. t.Errorf() records a failure without stopping; t.Fatalf() stops the test.
io.Reader in practice
import (
"io"
"the"
"strings"
)
r := strings.NewReader("hello world")
// copy from a Reader to a Writer
io.Copy(os.Stdout, r)
// read everything
data, _ := io.ReadAll(r)
// a file is a Reader
f, _ := os.Open("f.txt")
io.Copy(os.Stdout, f)io.Reader is the central input abstraction. io.Copy() transfers data from a Reader to a Writer efficiently. Files, strings and HTTP responses are all Readers.
Table-driven tests
func TestAdd(t *testing.T) {
tests := []struct {
a, b, want int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tt := range tests {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("Add(%d,%d) = %d; quero %d",
tt.a, tt.b, got, tt.want)
}
}
}Table-driven tests define several cases in a slice of structs and iterate over them with range. It's the idiomatic pattern in Go: compact, easy to extend and with clear error messages.
JSON
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
// struct -> JSON
data, _ := json.Marshal(Person{"Anna", 30})
// JSON -> struct
var p Person
json.Unmarshal(data, &p)
// with formatting
json.MarshalIndent(p, "", " ")The encoding/json package serializes structs. Marshal() converts to JSON and Unmarshal() does the reverse. The json:"name" tags define the field names in the JSON.
go vet and formatting
# static analysis go vet ./... # format go fmt ./... # format + organize imports goimports -w . # full linter (external) golangci-lint run
go vet detects suspicious errors that compile but are wrong. go fmt formats code in the official style. goimports also organizes imports automatically.
Generics (Go 1.18+)
// type parameter
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Max(3, 7) // 7
Max("a", "b") // "b"
// generic slice
func Map[T, U any](s []T, f func(T) U) []U {
res := make([]U, len(s))
for i, v := range s {
res[i] = f(v)
}
return res
}Generics allow functions and types parameterized by type, with [T ...]. The constraints.Ordered constraint limits T to orderable types. It reduces duplicated code while keeping type safety.