Go is a deliberately small language. It gives you one loop keyword, no exceptions, no inheritance, and a concurrency model built into the syntax rather than a library.
This article will teach you Go or Golang syntax and all important stuffs in one sitting. If you have a Go interview it's a quick recap for you and if you're learning it first time then also it'll teach you Go faster than any tutorial. Every concept is paired with code you can run.
If you're preparing for a backend round, read it alongside the framework-agnostic Top 111 Backend Interview Questions. If in case you happen to be a PHP developer as well, you can read PHP Syntax Recap.
How to Read and Run Go
Every Go program is a set of packages, and an executable starts in package main at the func main() entry point. Imports are explicit and, importantly, an unused import or an unused local variable is a compile error, not a warning, which keeps files clean but surprises newcomers. Identifiers that start with an uppercase letter are exported (visible outside their package); lowercase ones are package-private. Go inserts semicolons for you, so you never write them, and gofmt enforces one canonical formatting so style debates do not exist.
package main
import (
"fmt" // used below; an unused import would fail to compile
"strings"
)
// main is the entry point of an executable program.
func main() {
msg := strings.ToUpper("hello")
fmt.Println(msg) // HELLO
}
The commands you actually use are few:
go run main.gocompiles and runs in one step, for quick iteration.go buildproduces a standalone binary with no runtime dependency.go test ./...runs every test in the module.go vet ./...catches suspicious code that compiles but is probably wrong.gofmt -w .(orgo fmt) formats your code to the canonical style.
Variables and Types
Declaring variables
Go gives you two ways to declare a variable, and the difference is where you can use each. The var keyword works everywhere and lets you state the type explicitly; the short form := infers the type from the value but only works inside a function. Every variable you declare must be used, and you can assign several at once.
var count int = 42 // explicit type
var name = "Ada" // type inferred as string
age := 36 // short form; only inside functions
x, y := 1, 2 // multiple assignment
x, y = y, x // swap without a temp variable
var ready bool // declared without a value: gets the zero value (false)
Zero values
Go has no concept of an uninitialized variable: anything you declare without a value gets a well-defined zero value, which removes a whole class of "undefined" bugs. Knowing the zero values matters because idiomatic Go relies on them (a zero-value sync.Mutex is ready to use, a nil slice is safe to append to).
- Numeric types (
int,float64, and so on):0 bool:falsestring:""(empty, not nil)- Pointers, slices, maps, channels, functions, interfaces:
nil - A struct: every field set to its own zero value
The basic types
- Booleans:
bool. - Integers:
int,int8,int16,int32,int64and unsigneduint,uint8...uint64. Plainintis 64-bit on modern platforms. - Aliases you see constantly:
byte(an alias foruint8) andrune(an alias forint32, used for a Unicode code point). - Floats:
float32,float64(default for a decimal literal). - Strings:
string, which is immutable and holds UTF-8 bytes.
Type conversions
Go never converts between types implicitly, not even between numeric types, so you convert explicitly with T(v). This is stricter than C, Java, or JavaScript, and it is intentional: an unexpected int-to-float promotion cannot silently change a result.
i := 42
f := float64(i) // int to float64, required; there is no implicit promotion
u := uint(f) // float64 to uint (truncates toward zero)
b := []byte("hi") // string to byte slice
s := string(b) // byte slice back to string
r := string(rune(65)) // "A": an int code point to its string, via rune
Named types and constants
You create a new named type with type, which is how you give meaning to a plain value (a type UserID int cannot be mixed up with an ordinary int by accident). Constants are declared with const, and the iota counter inside a const block generates successive values, which is Go's idiom for enumerations.
type Celsius float64 // a distinct type, not just an alias
type Status int
const (
Pending Status = iota // 0
Active // 1 (iota increments automatically)
Archived // 2
)
const MaxRetries = 3 // untyped constant, adapts to context
Operators
Go's operators are conventional, with a few pointed differences from other languages worth flagging up front:
- Arithmetic:
+ - * / %, where/on two integers truncates toward zero and%is integer remainder. There is no**operator; usemath.Powfor exponentiation. - Comparison:
== != < <= > >=. Two structs are comparable with==if all their fields are comparable, which is convenient and has no equivalent in most languages. - Logical:
&& || !, with short-circuit evaluation. - Bitwise:
& | ^ << >>plus&^, the "AND NOT" (bit clear) operator that is unusual to Go. - Assignment:
=,:=, and the compound forms+=,-=, and so on.
Two things routinely trip people up. First, Go has no ternary operator, so a conditional value needs a full if/else. Second, x++ and x-- are statements, not expressions, so you cannot write y := x++ or arr[x++].
7 / 2 // 3 (integer division truncates)
7 % 2 // 1
1 << 4 // 16 (left shift)
0b1100 &^ 0b0100 // 8: clears the bits set in the right operand
count++ // a statement on its own line, never inside an expression
Strings, Bytes, and Runes
Strings are immutable UTF-8 bytes
A Go string is a read-only slice of bytes holding UTF-8 text, so indexing with s[i] gives you a single byte, not a character, and len(s) returns the number of bytes. To iterate over actual characters (runes), use range, which decodes one UTF-8 code point per step and gives you its starting byte index. This distinction is the source of most string bugs when the text is not plain ASCII.
s := "héllo"
len(s) // 6, not 5: 'é' takes two bytes in UTF-8
s[0] // 104, the byte value of 'h' (a byte, not "h")
for i, r := range s { // r is a rune (int32), i is the byte offset
fmt.Printf("%d:%c ", i, r) // 0:h 1:é 3:l 4:l 5:o
}
utf8.RuneCountInString(s) // 5: the real character count
Raw strings and conversions
Backtick-delimited raw string literals span multiple lines and ignore escape sequences, which is ideal for regexes, JSON blobs, and templates. Because strings are immutable, building one up character by character with + reallocates every time; use strings.Builder in a loop to avoid that cost.
raw := `C:\path\no\escapes
spanning multiple lines` // no \n processing, no escaping needed
var b strings.Builder
for i := 0; i < 3; i++ {
b.WriteString("go") // efficient: no per-iteration reallocation
}
b.String() // "gogogo"
The strings and strconv packages
String operations live in the strings package (there are no string methods), and conversions between strings and numbers live in strconv.
strings.Contains("seafood", "foo") // true
strings.HasPrefix("golang", "go") // true
strings.Index("chicken", "ken") // 4 (-1 if absent)
strings.Split("a,b,c", ",") // ["a", "b", "c"]
strings.Join([]string{"a", "b"}, "-")// "a-b"
strings.ReplaceAll("aaa", "a", "b") // "bbb"
strings.ToUpper("hi") // "HI"
strings.TrimSpace(" hi ") // "hi"
strings.Fields(" a b c ") // ["a", "b", "c"] (split on any whitespace)
strings.Repeat("ab", 3) // "ababab"
strconv.Atoi("42") // 42, nil (string to int, returns an error)
strconv.Itoa(42) // "42"
strconv.ParseInt("ff", 16, 64) // 255, nil (base 16)
strconv.ParseFloat("3.14", 64) // 3.14, nil
strconv.FormatInt(255, 16) // "ff"
The trap to remember: strconv.Atoi returns two values, the number and an error, so you must handle the error rather than ignore it, which is how Go signals that "abc" is not a valid integer.
Arrays, Slices, and Maps
Arrays versus slices
Go has both fixed-size arrays and dynamically-sized slices, and in practice you almost always use slices. An array's length is part of its type ([3]int and [4]int are different types) and assigning or passing an array copies it, which is why arrays are rare. A slice is a lightweight view into an underlying array, so it is cheap to pass around and is what every collection API expects.
var arr [3]int // a fixed array of 3 ints, all zero: [0 0 0]
arr[0] = 1
s := []int{1, 2, 3} // a slice literal (note: no size in the brackets)
s = append(s, 4) // grow it; append returns a new slice header
first3 := s[0:3] // slicing: elements 0, 1, 2 (half-open range)
How slices actually work
A slice is a small three-field header: a pointer to a backing array, a length (how many elements are in view), and a capacity (how many the backing array can hold from the pointer onward). Understanding this explains every slice surprise:
len(s)is the number of usable elements;cap(s)is how far it can grow before reallocating.appendwrites into spare capacity if there is room, mutating the shared backing array in place; when there is no room, it allocates a new, larger array and copies, so the original and the result no longer share memory.- Because
appendmay or may not reallocate, you must always assign its result back:s = append(s, x). - Two slices created from the same array share elements, so writing through one is visible through the other until one of them reallocates.

Working with slices
s := make([]int, 3, 10) // len 3, cap 10; the 3 elements are zero
len(s) // 3
cap(s) // 10
s = append(s, 1, 2) // append several at once
s = append(s, other...) // spread another slice with ...
dst := make([]int, len(src))
copy(dst, src) // copy element-by-element into an existing slice
// Remove index i (order not preserved cheaply):
s = append(s[:i], s[i+1:]...)
// A nil slice is a valid empty slice: len 0, and append works on it.
var empty []int
empty = append(empty, 1) // fine, no make() needed
Two-dimensional slices
Go has no built-in 2D slice, so you build a slice of slices, allocating each row. This matters in grid problems, where forgetting to allocate the inner rows gives you a nil-slice panic on write.
rows, cols := 3, 4
grid := make([][]int, rows)
for i := range grid {
grid[i] = make([]int, cols) // each row must be allocated
}
grid[2][3] = 9
Maps
A map is Go's hash table, written map[Key]Value. You create one with make or a literal, and reading a missing key returns the value's zero value rather than an error, so Go gives you the "comma ok" form to tell "missing" apart from "present but zero." Iteration order is deliberately randomized, so never rely on it.
m := map[string]int{"ada": 90, "lin": 70}
m["joy"] = 85 // insert or update
score := m["ada"] // 90
missing := m["nobody"] // 0, the zero value (no error)
v, ok := m["joy"] // comma-ok: v is 85, ok is true
_, exists := m["nobody"] // exists is false
delete(m, "lin") // remove a key
len(m) // number of entries
for key, val := range m { // NOTE: iteration order is random each run
_ = key; _ = val
}
Go has no built-in set, so the idiom is a map whose value is the empty struct struct{}{}, which takes zero bytes:
seen := map[string]struct{}{}
seen["a"] = struct{}{} // add
if _, ok := seen["a"]; ok { // membership test
// present
}
The slices and maps packages
Since Go 1.21 the standard library includes generic helpers that replace a lot of hand-written loops, plus min, max, and clear as builtins.
slices.Sort(s) // sort in ascending order, in place
slices.Contains(s, 3) // true if 3 is present
slices.Index(s, 3) // first index of 3, or -1
slices.Max(s); slices.Min(s) // largest / smallest element
slices.Reverse(s) // reverse in place
slices.Equal(a, b) // element-wise equality
min(3, 7) // 3 (builtin since Go 1.21)
max(3, 7) // 7
clear(m) // remove all entries from a map
In a coding round, slices and maps cover almost everything: a map is your hash map and, with struct{} values, your set; a []T is your dynamic array, stack (append and re-slice), and queue (though popping the front is O(n), so use a real structure for heavy queue work).
Control Flow
if and switch
An if has no parentheses around the condition and can start with a short init statement, scoped to the if, which is the idiomatic place to call a function and check its error in one line. A switch in Go does not fall through by default (each case breaks on its own), you can omit the condition to write a clean if/else chain, and a type switch branches on the dynamic type of an interface value.
if v := compute(); v > 0 { // init statement + condition; v is scoped here
fmt.Println(v)
} else {
fmt.Println("non-positive")
}
switch day {
case "sat", "sun": // multiple values per case
fmt.Println("weekend")
case "mon":
fmt.Println("monday")
fallthrough // opt in to fall through explicitly
default:
fmt.Println("weekday")
}
switch { // no condition: acts like if/else-if
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
}
The for loop
Go has exactly one loop keyword, for, which covers every looping need in four shapes:
- Three-clause C-style:
for i := 0; i < n; i++ { }. - Condition-only, which is Go's
while:for cond { }. - No clause, an infinite loop you exit with
break:for { }. - Range form over a collection:
for i, v := range s { }.
range adapts to what you iterate, and since Go 1.22 you can even range over an integer:
for i, v := range []string{"a", "b"} { } // index and value of a slice
for k, v := range someMap { } // key and value of a map (random order)
for i, r := range "héllo" { } // byte index and rune of a string
for v := range ch { } // values from a channel until it closes
for i := range 5 { } // 0,1,2,3,4 (Go 1.22+: range over an int)
break and continue accept a label so you can escape or skip an outer loop from inside a nested one, which is invaluable for grid and matrix problems:
outer:
for _, row := range grid {
for _, cell := range row {
if cell == target {
break outer // exit both loops at once
}
}
}
Functions
Multiple return values and named returns
Functions are first-class values, and the defining Go feature is that they return multiple values, which is how the language reports errors without exceptions. You can name the return values, which documents them and lets you use a bare return, though heavy use of named returns tends to hurt readability.
func divmod(a, b int) (int, int) { // two return values
return a / b, a % b
}
q, r := divmod(17, 5) // 3, 2
func split(sum int) (x, y int) { // named returns
x = sum * 4 / 9
y = sum - x
return // bare return uses the named values
}
Variadic functions and closures
A variadic parameter, written ...T, accepts any number of trailing arguments as a slice, and you spread an existing slice into it with .... A closure is a function literal that captures variables from its surrounding scope by reference, which is how you carry state into callbacks.
func sum(nums ...int) int { // nums is a []int inside the function
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6
sum(existing...) // spread a slice
func counter() func() int { // returns a closure
count := 0
return func() int {
count++ // captures and mutates count
return count
}
}
next := counter()
next(); next() // 1, then 2
defer, panic, and recover
defer schedules a function call to run when the surrounding function returns, no matter how it returns, which is the clean way to release resources such as files, locks, and connections. Deferred calls run in last-in-first-out order, and their arguments are evaluated at the point of defer, not when they actually run, which is a common surprise.
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when readFile returns, however it returns
i := 0
defer fmt.Println("deferred i =", i) // prints 0: i was captured at defer time
i = 99
return nil
}
panic stops normal flow and unwinds the stack running deferred calls, and recover, called inside a deferred function, stops that unwinding and returns the panic value. This pair is Go's last-resort mechanism for truly exceptional situations, not its everyday error handling.
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil { // catch the panic
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil // a divide-by-zero panics; recover turns it into err
}
Structs, Methods, and Interfaces
Go has no classes and no inheritance. Instead it composes behavior from structs, methods, and interfaces, which together cover what other languages do with objects but with different rules.
Structs
A struct groups named fields into a single type. You create one with a literal (positional or, better, by field name), and Go structs are value types, so assigning one copies every field. Two structs of the same type are comparable with == when all their fields are comparable. Struct tags, the backtick strings after a field, attach metadata that packages like encoding/json read at runtime.
type User struct {
ID int
Name string `json:"name"` // struct tag: JSON key becomes "name"
Email string `json:"email,omitempty"`
}
u := User{ID: 1, Name: "Ada"} // named fields (preferred)
u2 := User{2, "Lin", "[email protected]"} // positional (fragile; avoid)
p := &User{Name: "Joy"} // &T{} makes a pointer to a new struct
u.Name // field access with . on value or pointer
p.Name // Go auto-dereferences: no (*p).Name needed
Methods and receivers
A method is a function with a receiver argument written before the name, which binds it to a type. The receiver can be a value (the method gets a copy) or a pointer (the method can mutate the original). Choosing between them follows clear rules:
- Use a pointer receiver when the method modifies the receiver, or when the struct is large and you want to avoid copying it.
- Use a value receiver for small, immutable types where a copy is cheap and mutation is not wanted.
- Be consistent: if any method needs a pointer receiver, give all methods on that type pointer receivers, so the method set stays uniform.
type Counter struct{ n int }
func (c Counter) Value() int { return c.n } // value receiver: reads a copy
func (c *Counter) Inc() { c.n++ } // pointer receiver: mutates the original
c := Counter{}
c.Inc() // Go takes the address automatically for you
c.Value() // 1
Embedding for composition
Go replaces inheritance with embedding: put one type inside another without a field name, and its fields and methods are promoted to the outer type. The outer type does not become a subtype, but it gains the embedded behavior, which is composition made syntactically convenient.
type Animal struct{ Name string }
func (a Animal) Speak() string { return a.Name + " makes a sound" }
type Dog struct {
Animal // embedded (no field name)
Breed string
}
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Lab"}
d.Name // "Rex": promoted from Animal
d.Speak() // "Rex makes a sound": promoted method
Interfaces
An interface is a set of method signatures, and a type satisfies it implicitly simply by having those methods; there is no implements keyword and no declared relationship. This is the single biggest design difference from Java or C#, and it is why Go interfaces are usually tiny (often one method) and defined by the code that consumes them, not the code that provides them.
type Shape interface {
Area() float64 // any type with this method satisfies Shape
}
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
// Circle satisfies Shape automatically; no declaration is needed.
var s Shape = Circle{R: 2}
s.Area() // 12.566...
The empty interface, spelled any since Go 1.18, holds a value of any type. You get the concrete value back out with a type assertion (in comma-ok form so a wrong guess does not panic) or a type switch.
var x any = "hello"
str, ok := x.(string) // type assertion, comma-ok form: str="hello", ok=true
n, ok := x.(int) // ok=false, n=0 (no panic because of the comma-ok form)
switch v := x.(type) { // type switch: branch on the dynamic type
case string:
fmt.Println("string of length", len(v))
case int:
fmt.Println("int", v)
default:
fmt.Println("something else")
}
An interface value is a (type, value) pair
Internally, an interface value holds two words: the concrete type it currently stores and a pointer to the value. An interface is nil only when both are nil. This is the mechanism behind the notorious typed-nil bug: if you store a nil pointer of a concrete type in an interface, the interface holds a non-nil type, so it compares as not equal to nil even though the underlying pointer is nil.

Error Handling
Errors are values
Go has no exceptions for ordinary failures. Instead a function that can fail returns an error as its last value, and the caller checks it immediately; error is just an interface with a single Error() string method. This makes the failure path explicit and local, at the cost of the repetitive if err != nil you will write constantly.
f, err := os.Open("config.yaml")
if err != nil {
return err // handle or propagate; do not ignore
}
defer f.Close()
// Create simple errors with errors.New or fmt.Errorf:
err1 := errors.New("not found")
err2 := fmt.Errorf("user %d: %w", id, err1) // %w wraps err1 so it can be unwrapped
Wrapping, Is, and As
Wrapping an error with the %w verb preserves the original while adding context, building a chain you can inspect. You compare against a known sentinel error with errors.Is, and you extract a specific error type with errors.As; never compare wrapped errors with ==, because the wrapper is a different value.
var ErrNotFound = errors.New("not found") // a sentinel error
func lookup(id int) error {
return fmt.Errorf("lookup %d: %w", id, ErrNotFound) // wrap the sentinel
}
err := lookup(7)
errors.Is(err, ErrNotFound) // true: unwraps the chain to find the sentinel
var pathErr *os.PathError
if errors.As(err, &pathErr) { // true if some error in the chain is *os.PathError
fmt.Println(pathErr.Path)
}
Custom error types
Any type with an Error() string method is an error, so you define a custom error to carry structured data callers can act on.
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return e.Field + ": " + e.Msg
}
func validate(name string) error {
if name == "" {
return &ValidationError{Field: "name", Msg: "required"}
}
return nil
}
When deciding between returning an error and panicking, the rule of thumb is:
- Return an
errorfor anything a caller could reasonably expect and handle: missing files, bad input, network failures. This is the default. paniconly for programmer mistakes and truly unrecoverable states: an impossible switch case, a broken invariant, a nil pointer that should never be nil.recoverat trust boundaries (an HTTP handler, a worker goroutine) so one request's panic does not crash the whole process.
Concurrency
Concurrency is built into Go rather than added by a library, which is its headline feature. The model is "do not communicate by sharing memory; share memory by communicating," meaning you coordinate goroutines with channels instead of locks wherever you can. For the conceptual difference between concurrency and parallelism, the Top 111 Backend Interview Questions covers it directly.
Goroutines
A goroutine is a function running concurrently, started by putting go in front of a call. Goroutines are extremely cheap (a few kilobytes of stack, multiplexed onto OS threads by the runtime), so you can have hundreds of thousands. The catch is that main does not wait for them, so you need a way to synchronize, or the program exits while goroutines are still running.
go doWork() // runs concurrently; main does not wait
go func() { // an anonymous goroutine
fmt.Println("in a goroutine")
}()
Channels
A channel is a typed conduit you send to with ch <- v and receive from with v := <-ch. Channels come in two kinds, and the difference decides whether operations block:
- An unbuffered channel (
make(chan int)) has no capacity: a send blocks until another goroutine receives, so it synchronizes the two sides at that moment. - A buffered channel (
make(chan int, n)) holds up tonvalues: a send blocks only when the buffer is full, and a receive blocks only when it is empty.
ch := make(chan int) // unbuffered
go func() { ch <- 42 }() // this send blocks until the receive below runs
v := <-ch // 42
buf := make(chan int, 2) // buffered, capacity 2
buf <- 1 // does not block
buf <- 2 // does not block; a third send would block
close(buf) // signal that no more values will be sent
for v := range buf { // ranges until the channel is closed and drained
fmt.Println(v) // 1, then 2
}
v, ok := <-buf // ok is false once a closed channel is drained

select
select waits on several channel operations at once and proceeds with whichever is ready first, picking randomly among several ready cases. A default case makes the whole select non-blocking, and a time.After case gives you a timeout.
select {
case v := <-ch1:
fmt.Println("from ch1:", v)
case ch2 <- 99:
fmt.Println("sent to ch2")
case <-time.After(time.Second):
fmt.Println("timed out after 1s")
default:
fmt.Println("nothing ready right now") // makes select non-blocking
}
Synchronization with the sync package
When channels are the wrong tool (usually for guarding shared state), the sync package provides classic primitives:
sync.WaitGroupwaits for a set of goroutines to finish: callAdd(n), each goroutine callsDone(), and the main goroutine callsWait().sync.Mutexgives mutual exclusion withLock()andUnlock(), usually paired withdefer mu.Unlock().sync.RWMutexallows many concurrent readers or one writer.sync.Onceruns an initialization exactly once, even under concurrent callers.
var wg sync.WaitGroup
results := make([]int, 5)
for i := range 5 {
wg.Add(1)
go func() { // Go 1.22+: i is per-iteration, safe to use directly
defer wg.Done()
results[i] = i * i // distinct indices, so no lock needed here
}()
}
wg.Wait() // block until all five goroutines call Done
var mu sync.Mutex
mu.Lock()
counter++ // protect shared mutable state
mu.Unlock()
Run tests and programs with the -race flag (go run -race .) to catch data races, where two goroutines touch the same memory and at least one writes; the race detector is the fastest way to find these bugs.
Generics
Since Go 1.18 functions and types can take type parameters, letting you write one implementation that works for many types with full type safety, instead of falling back to any and type assertions. A type parameter has a constraint that limits which types are allowed: any allows anything, comparable allows types usable with ==, and you can define your own constraint as an interface of allowed types.
// A generic function: works for any element type.
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 }) // [2, 4, 6]
// A custom constraint: the ~ allows named types whose underlying type matches.
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](s []T) T {
var total T
for _, v := range s {
total += v
}
return total
}
Sum([]int{1, 2, 3}) // 6
Standard Library Essentials
Formatting and printing with fmt
fmt handles formatted I/O. Println prints values separated by spaces, Printf uses a format string, and Sprintf returns the formatted string instead of printing it. The verbs you actually use:
%vdefault format,%+vadds struct field names,%#vprints Go syntax.%Tthe value's type.%dinteger,%bbinary,%xhex,%ooctal.%sstring,%qa double-quoted string,%ca rune as a character.%ffloat,%.2ffixed precision,%gcompact float,%escientific.%tboolean,%ppointer.
fmt.Printf("%s is %d (%.1f%%)\n", "cpu", 3, 42.5) // cpu is 3 (42.5%)
fmt.Printf("%+v\n", User{ID: 1, Name: "Ada"}) // {ID:1 Name:Ada Email:}
s := fmt.Sprintf("%03d", 7) // "007"
Fast input and output
Reading input with fmt.Scan is fine for a few values but slow for large inputs, which matters in timed rounds. The fast idiom is a buffered bufio.Scanner for reading and a bufio.Writer for writing, flushed with a deferred Flush.
reader := bufio.NewScanner(os.Stdin)
reader.Buffer(make([]byte, 1024*1024), 1024*1024) // allow long lines
reader.Split(bufio.ScanWords) // token-by-token; default is by line
reader.Scan()
n, _ := strconv.Atoi(reader.Text()) // read one integer token
writer := bufio.NewWriter(os.Stdout)
defer writer.Flush() // MUST flush, or output is lost
fmt.Fprintln(writer, "answer:", n)
Sorting
Modern code sorts with the generic slices package, while a lot of existing code uses the older sort package; both are worth recognizing.
slices.Sort(nums) // ascending, in place (preferred)
slices.SortFunc(people, func(a, b Person) int { // custom order: return <0, 0, >0
return cmp.Compare(a.Age, b.Age)
})
sort.Ints(nums) // the older API
sort.Slice(people, func(i, j int) bool { // less-than comparator
return people[i].Age < people[j].Age
})
Heaps and other containers
For priority-queue problems, container/heap turns any type that implements its interface into a heap. It is more verbose than other languages' heaps because you supply the ordering, but the pattern is fixed and worth memorizing.
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } // < gives a min-heap
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
h := &IntHeap{5, 1, 3}
heap.Init(h) // establish the heap invariant
heap.Push(h, 2)
heap.Pop(h) // 1 (the smallest, because Less uses <)
Math and randomness
math.Abs(-3.5) // 3.5 (operates on float64)
math.Sqrt(144) // 12
math.Pow(2, 10) // 1024
math.MaxInt, math.MinInt // integer bounds
math.Inf(1) // positive infinity
rand.Intn(6) // a pseudo-random int in [0, 6)
Sharp Edges
A handful of Go behaviors reliably catch both newcomers and returning developers. Each item below pairs the trap with its fix.
- Nil maps panic on write. A map declared as
var m map[string]intis nil, and assigning to it crashes; initialize it withmakeor a literal first. Nil slices are the exception, since you canappendto them directly. - Slices share a backing array. A sub-slice points into the same memory, so writes leak across both views, and
appendmay quietly reallocate. Always reassigns = append(s, x), and reach forslices.Clonewhen you need an independent copy. rangecopies each element. Infor _, v := range s,vis a copy, so writing to it changes nothing. Index the slice instead:for i := range s { s[i] = ... }.- A typed nil is not a nil interface. Returning a nil pointer as an
errorproduces an interface that still carries a type, so the caller'serr != nilis true. Return a literalnilon the success path. - Map iteration order is randomized. Go shuffles
rangeorder over a map on every run, so never depend on it. When you need determinism, collect the keys, sort them, and iterate that slice. - Integer math has sharp corners.
7 / 2is3because both operands are ints, integer divide-by-zero panics at runtime, and overflow wraps silently (int8(127) + 1is-128). Widen the type, or reach formath/big. ==misses wrapped errors. Once an error is wrapped with%w,err == ErrNotFoundis false. Useerrors.Isfor sentinel values anderrors.Asfor error types, both of which walk the wrap chain.- Unbuffered channels deadlock alone. A send blocks until another goroutine receives, so sending from the only running goroutine hangs the whole program. Make sure a receiver exists, or give the channel a buffer.
- Unused imports and variables fail the build. Go treats them as errors, not warnings. Discard a value with the blank identifier
_, and delete any import you are not using.




