Program Structure
Every Go source file belongs to a package. An executable program must have a package main with a func main() entry point. Imports are grouped, and unused imports are a compile error, which keeps files tidy.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("hello, go"))
}
Exported names
Identifiers that start with a capital letter are exported (public) from a package. Lowercase names are package-private. There is no public/private keyword.
Variables & Constants
Declare with var, or use the short form := inside functions. Constants are compile-time values, and iota generates incrementing enum-like values.
var count int = 10 // explicit type
var name = "Ada" // inferred type
age := 30 // short form (function scope only)
var x, y = 1, 2 // multiple
const Pi = 3.14159
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
)
const (
_ = iota // skip 0
KB = 1 << (10 * iota) // 1024
MB // 1048576
)
Basic Types
Go has fixed-width numeric types, a native bool, and immutable UTF-8 string. The zero value of any variable is well-defined (0, "", false, nil).
| Category | Types |
|---|---|
| Integers | int, int8..int64, uint, uintptr |
| Floats / Complex | float32, float64, complex128 |
| Text | string, rune (int32), byte (uint8) |
| Other | bool, error |
Functions
Functions can return multiple values, which powers Go's error-handling idiom. They also support variadic parameters, closures, and defer for cleanup.
// multiple return values
func divmod(a, b int) (int, int) {
return a / b, a % b
}
q, r := divmod(17, 5)
// variadic
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6
sum([]int{1, 2}...) // spread a slice
// closure: captures counter
func counter() func() int {
i := 0
return func() int { i++; return i }
}
defer schedules a call to run when the surrounding function returns. Deferred calls run in LIFO order and are ideal for closing resources.
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when readFile returns
// ... use f
return nil
}
Structs & Methods
A struct groups named fields. Methods attach to a type via a receiver. Choose a pointer receiver when the method mutates the value or the struct is large.
type Counter struct {
Name string
Total int
}
// value receiver: gets a copy, cannot mutate original
func (c Counter) Label() string {
return fmt.Sprintf("%s=%d", c.Name, c.Total)
}
// pointer receiver: can mutate the original
func (c *Counter) Inc() {
c.Total++
}
c := Counter{Name: "hits"}
c.Inc() // Go auto-takes &c
fmt.Println(c.Label())
| Aspect | Value receiver (c T) | Pointer receiver (c *T) |
|---|---|---|
| Sees a | Copy | The original |
| Can mutate | No (copy only) | Yes |
| Cost for large struct | Full copy each call | Cheap (one pointer) |
| Best for | Small, immutable types | Mutation, large types |
Be consistent
If any method on a type needs a pointer receiver, use pointer receivers for all its methods so the method set stays uniform.
Interfaces
Interfaces are satisfied implicitly: any type that has the required methods satisfies the interface, with no implements keyword. This decouples concrete types from the abstractions that use them.
type Stringer interface {
String() string
}
type Point struct{ X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
var s Stringer = Point{1, 2} // Point satisfies Stringer
The empty interface interface{} (aliased as any since Go 1.18) holds any value. Recover the concrete type with a type assertion or a type switch.
var v any = "hello"
// type assertion (comma-ok form is safe)
if s, ok := v.(string); ok {
fmt.Println(len(s))
}
// type switch
func describe(v any) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("int %d", x)
case string:
return fmt.Sprintf("string %q", x)
default:
return "unknown"
}
}
Arrays, Slices & Maps
Arrays have a fixed length baked into the type. Slices are the everyday workhorse: a lightweight view over a backing array with a length and capacity. make preallocates and append grows a slice, reallocating when capacity is exceeded.
var arr [3]int // array, length is part of type
nums := []int{1, 2, 3} // slice literal
s := make([]int, 0, 8) // len 0, cap 8
s = append(s, 1, 2, 3)
fmt.Println(len(s), cap(s)) // 3 8
sub := nums[1:3] // slice of nums, shares backing array
// maps
m := map[string]int{"a": 1}
m["b"] = 2
v, ok := m["a"] // comma-ok: ok is false if missing
delete(m, "a")
for k, val := range m { // iteration order is randomized
fmt.Println(k, val)
}
Slices share memory
A re-slice points at the same backing array, so writes through one slice can be seen through another. Use copy(dst, src) when you need an independent copy.
Error Handling
Go treats errors as ordinary values. The built-in error interface has a single Error() string method. Functions return an error as the last value, and callers check if err != nil.
func parse(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
// wrap with %w to preserve the chain
return 0, fmt.Errorf("parse %q: %w", s, err)
}
return n, nil
}
var ErrNotFound = errors.New("not found")
_, err := parse("abc")
if errors.Is(err, strconv.ErrSyntax) { /* sentinel match */ }
var numErr *strconv.NumError
if errors.As(err, &numErr) { // extract typed error
fmt.Println(numErr.Func)
}
Reserve panic for truly unrecoverable situations. A deferred recover can catch a panic and turn it back into an error at a boundary.
func safeRun() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
panic("boom")
}
Generics
Since Go 1.18, functions and types can take type parameters in square brackets, bounded by constraints. The any constraint allows any type; comparable allows ==; custom constraints use interface unions.
// generic map over a slice
func Map[T, U any](s []T, f func(T) U) []U {
out := make([]U, len(s))
for i, v := range s {
out[i] = f(v)
}
return out
}
doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })
// constraint via union of underlying types
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
The standard library ships generic helpers in slices and maps, and ordering constraints live in cmp.Ordered.
Concurrency
A goroutine is a lightweight thread launched with the go keyword. Goroutines communicate over channels rather than sharing memory directly: "Don't communicate by sharing memory; share memory by communicating."
ch := make(chan int) // unbuffered
buf := make(chan int, 4) // buffered, capacity 4
go func() {
ch <- 42 // send blocks until received
}()
v := <-ch // receive
close(buf)
for x := range buf { // ranges until channel is closed
fmt.Println(x)
}
| Operation | Syntax | Behavior |
|---|---|---|
| Send | ch <- v | Blocks until a receiver (or buffer space) is ready |
| Receive | v := <-ch | Blocks until a value is available |
| Receive + ok | v, ok := <-ch | ok is false when the channel is closed and drained |
| Directional | chan<- int / <-chan int | Send-only / receive-only channel type |
select waits on multiple channel operations; sync.WaitGroup waits for a group of goroutines to finish; sync.Mutex protects shared state.
select {
case v := <-ch:
fmt.Println("got", v)
case <-time.After(time.Second):
fmt.Println("timeout")
default:
fmt.Println("nothing ready")
}
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Println("worker", id)
}(i)
}
wg.Wait()
var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()
context.Context carries cancellation signals and deadlines across API boundaries and goroutines. Always pass it as the first argument.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-ctx.Done():
fmt.Println("cancelled:", ctx.Err())
case result := <-work(ctx):
fmt.Println(result)
}
Detect data races
Run tests and binaries with the -race flag (go test -race ./...) to catch unsynchronized concurrent access at runtime.
Standard Library Highlights
Go's batteries-included standard library covers most day-to-day needs. A few workhorses:
// net/http: a full server in a few lines
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hi")
})
http.ListenAndServe(":8080", nil)
// encoding/json
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
}
data, _ := json.Marshal(User{Name: "Ada", Age: 36})
var u User
json.Unmarshal(data, &u)
// os & io
b, _ := os.ReadFile("config.txt")
io.Copy(os.Stdout, strings.NewReader("stream this"))
Tooling & Modern Features
The go command is the single entry point for building, testing, and dependency management via modules.
go mod init example.com/app # start a module
go get github.com/pkg/errors # add a dependency
go mod tidy # sync go.mod / go.sum
go build ./... # compile everything
go run . # build and run
go test ./... -race -cover # test with race + coverage
go vet ./... # static analysis
gofmt -w . # format in place
Recent releases removed long-standing footguns. Since Go 1.22, each loop iteration gets its own copy of the loop variable, so the classic goroutine-capture bug is gone. Go 1.22 also added range-over-integer, and Go 1.23 added range-over-function iterators.
// Go 1.22+: range over an int
for i := range 5 {
fmt.Println(i) // 0 1 2 3 4
}
// Go 1.22+: loop var is per-iteration; this is now safe
for _, v := range items {
go func() { process(v) }() // v is distinct each iteration
}
Practice Exercises
- Write a
Stack[T any]generic type withPush,Pop, andLenmethods, wherePopreturns a value and anerrorwhen empty. - Implement a
Shapeinterface with anArea() float64method, then satisfy it withCircleandRectangleand sum a slice of shapes. - Build a worker pool: fan out N goroutines that read jobs from a channel and write results to another channel, coordinating completion with a
sync.WaitGroup. - Write a function that wraps an error with
%wand a caller that uses botherrors.Isanderrors.Asto inspect the chain. - Create an HTTP server with
net/httpthat returns JSON, and enforce a request timeout usingcontext.Context. - Use
selectwith atime.Aftertimeout to consume from a channel, printing "timeout" if no value arrives within one second.