Golang / GoLang Basics Interview Questions
Go (also called Golang) is an open-source, statically typed, compiled programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. It was announced in 2009 and reached version 1.0 in 2012.
The creators were frustrated with the tools available at Google. C++ compile times were painfully slow on massive codebases, Java was verbose and required heavy infrastructure, and dynamically typed languages lacked compile-time safety. They wanted a language that combined the performance of C, the readability of Python, and first-class support for concurrency on multicore hardware.
| Goal | How Go achieves it |
|---|---|
| Fast compilation | Simple grammar, no header files, dependency graph resolved at compile time β even large codebases compile in seconds |
| Readable code | Minimal syntax, one way to do most things, gofmt enforces consistent formatting |
| Built-in concurrency | Goroutines and channels are language primitives, not library add-ons |
| Memory safety | Garbage collector, bounds checking, no manual malloc/free |
| Simple deployment | go build produces a single statically linked binary with no runtime dependencies |
// A complete Go program
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}Go is the language behind Docker, Kubernetes, Terraform, and many cloud-native tools. Companies like Google, Uber, Dropbox, and Cloudflare use it extensively for high-throughput backend services and CLI tooling.
Go has a deliberately small feature set. Every design decision was made by asking: does this add enough value to justify the complexity it introduces? The result is a language that experienced developers can learn in days and that reads consistently across large teams.
| Characteristic | Description |
|---|---|
| Statically typed | Types checked at compile time β type errors are found before the program runs |
| Compiled to native code | No VM or interpreter β source compiles directly to machine code for fast startup and execution |
| Garbage collected | Memory managed automatically; Go's concurrent GC has sub-millisecond pause targets |
| Goroutines & channels | Concurrency primitives built into the language, not bolted on as a library |
| No classes or inheritance | Structs + interfaces + embedding: composition over inheritance |
| Implicit interface satisfaction | A type implements an interface just by having the required methods β no 'implements' keyword |
| Multiple return values | Functions return (result, error) β the idiomatic error-handling pattern |
| Single binary output | go build produces one statically linked executable β trivial to deploy |
// Multiple return values β idiomatic Go
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
result, err := divide(10, 3)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%.2f\n", result) // 3.33Every Go source file starts with a package declaration. Packages are Go's unit of code organisation, encapsulation, and compilation. A package groups related types, functions, constants, and variables.
The main package is unique: it defines an executable program. The main() function within it is the program's entry point. Any package not named main is a library package β importable by others but not directly runnable.
Exported vs unexported: identifiers starting with an uppercase letter are exported (public). Lowercase identifiers are unexported (package-private). This is Go's entire visibility system β no public, private, or protected keywords.
// main.go β executable program
package main
import (
"fmt"
"myproject/mathutil" // importing a library package
)
func main() {
result := mathutil.Add(3, 4) // Add is exported (uppercase)
fmt.Println(result) // 7
}
// mathutil/math.go β library package
package mathutil
func Add(a, b int) int { return a + b } // exported
func helper(x int) int { return x * 2 } // unexported (private)Go offers several declaration styles. The choice between them is mostly about context (package level vs inside a function) and verbosity. Every variable is always initialised β Go has no uninitialized variables.
| Style | Where usable | Type required? | Notes |
|---|---|---|---|
| var name type | Anywhere | Yes | Initialised to zero value |
| var name = value | Anywhere | No β inferred | Useful when type is obvious from value |
| name := value | Functions only | No β inferred | Most common inside functions |
| var (name type; ...) | Anywhere | Yes | Groups multiple declarations |
// Package-level: var keyword required
var appName string = "MyApp"
var maxRetries = 3 // type inferred as int
// Function-level: short declaration preferred
func main() {
greeting := "Hello" // most common β := infers type
x, y := 10, 20 // multiple assignment
x, y = y, x // swap without temp variable!
// Blank identifier: discard unwanted values
result, _ := divide(10, 3) // ignore the error
// var block for related declarations
var (
firstName = "Alice"
lastName = "Smith"
age = 30
)
fmt.Println(greeting, result, firstName, lastName, age)
}
// Zero values β every variable gets one
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // ""Go has a concise but complete set of built-in primitive types. Choosing the right type β especially between int and sized integers, and between float32 and float64 β matters for correctness and interoperability.
| Category | Types | Common default |
|---|---|---|
| Signed integers | int8, int16, int32, int64, int | int (platform width: 64-bit on 64-bit OS) |
| Unsigned integers | uint8, uint16, uint32, uint64, uint | uint8 alias = byte |
| Floating point | float32, float64 | float64 (more precise; the default) |
| Boolean | bool | false |
| String | string | "" (immutable UTF-8 bytes) |
| Rune | rune (= int32) | represents a Unicode code point |
var i int = 42
var f float64 = 3.14159
var b byte = 255 // alias for uint8
var r rune = 'β‘' // alias for int32 β Unicode code point
var str string = "Hello, δΈη"
// Type conversions are ALWAYS explicit β no implicit casting
var x int = 100
var y float64 = float64(x) // must be explicit
var z int = int(y) // truncates decimal part
// String byte count vs character count (UTF-8)
fmt.Println(len(str)) // 13 bytes
fmt.Println(len([]rune(str))) // 9 runes/characters
// Constants are untyped by default β flexible in expressions
const Pi = 3.14159
const MaxItems = 1000Constants are declared with const and must be assigned a value that is computable at compile time β no function calls or runtime values. The iota identifier provides an automatically incrementing integer within a const block, resetting to 0 at the start of each new block.
// Simple constants
const Pi = 3.14159
const AppName = "MyService"
// iota: auto-incrementing integer, resets at each const block
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
// iota with bit-shifting β perfect for flag constants
type Permission uint
const (
Read Permission = 1 << iota // 1 (001)
Write // 2 (010)
Execute // 4 (100)
)
userPerms := Read | Write // 3 β can read and write
// iota with expressions
const (
_ = iota // skip 0
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20
GB // 1 << 30
)Functions are first-class citizens in Go β they can be assigned to variables, passed as arguments, and returned from other functions. Go functions support multiple return values (the primary mechanism for error handling), named returns, and variadic parameters.
// Basic function with multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
// Named return values β document what is returned
// Use sparingly; best for short functions only
func minMax(nums []int) (min, max int) {
min, max = nums[0], nums[0]
for _, n := range nums[1:] {
if n < min { min = n }
if n > max { max = n }
}
return // naked return β returns named values min and max
}
// Variadic function β accepts 0 or more arguments of the given type
func sum(nums ...int) int {
total := 0
for _, n := range nums { total += n }
return total
}
sum(1, 2, 3) // 6
s := []int{4, 5, 6}
sum(s...) // 15 β spread a slice with ...
// Function as a value (first-class)
double := func(n int) int { return n * 2 }
fmt.Println(double(7)) // 14
// Higher-order function
func apply(nums []int, f func(int) int) []int {
result := make([]int, len(nums))
for i, v := range nums { result[i] = f(v) }
return result
}
fmt.Println(apply([]int{1, 2, 3}, double)) // [2 4 6]Go has a deliberately minimal set of control-flow constructs. There is only one loop keyword β for β which covers everything a while, do-while, and classic for loop does in other languages.
// if with an init statement β err is only in scope inside the if block
if err := doWork(); err != nil {
log.Fatal(err)
}
// for β C-style
for i := 0; i < 5; i++ { fmt.Println(i) }
// for as while
n := 1
for n < 128 { n *= 2 }
// for β infinite loop (use break or return to exit)
for {
if done() { break }
doWork()
}
// for range β slices, maps, strings, channels
fruits := []string{"apple", "banana", "cherry"}
for i, fruit := range fruits {
fmt.Printf("%d: %s\n", i, fruit)
}
for _, fruit := range fruits { fmt.Println(fruit) } // ignore index
// switch β no implicit fallthrough
switch day {
case "Sat", "Sun":
fmt.Println("Weekend")
case "Mon", "Tue", "Wed", "Thu", "Fri":
fmt.Println("Weekday")
default:
fmt.Println("Unknown")
}
// switch with no expression β replaces long if-else chains
switch {
case score >= 90: fmt.Println("A")
case score >= 80: fmt.Println("B")
default: fmt.Println("C or below")
}Arrays and slices are both ordered sequences, but they work very differently. Arrays are value types with a fixed size baked into their type; slices are reference types that provide a flexible view into an underlying array.
| Aspect | Array | Slice |
|---|---|---|
| Size | Fixed at compile time; part of the type ([5]int β [6]int) | Dynamic β can grow via append |
| Passed to functions as | Full copy (value type β can be expensive) | 3-word header: pointer + len + cap (cheap) |
| Zero value | [0, 0, 0, ...] | nil (len=0, cap=0) |
| Common in practice | Rarely β mainly fixed-size buffers or keys | The everyday Go sequence type |
// Array
var arr [5]int // [0 0 0 0 0]
arr2 := [3]string{"a", "b", "c"}
arr3 := [...]int{1, 2, 3, 4} // compiler counts: [4]int
// Slice
s := []int{1, 2, 3} // slice literal
s2 := make([]int, 5) // len=5, cap=5, all zeros
s3 := make([]int, 3, 10) // len=3, cap=10
// append β returns a NEW slice (may reallocate backing array)
s = append(s, 4, 5) // [1 2 3 4 5]
// Sub-slice β shares backing array!
sub := s[1:4] // [2 3 4]
sub[0] = 99 // changes s[1] too!
// copy β independent backing array
dst := make([]int, len(s))
copy(dst, s)
// nil slice vs empty slice
var nilSlice []int // nil β len=0, cap=0
empty := []int{} // not nil β len=0, cap=0
fmt.Println(nilSlice == nil) // true
fmt.Println(empty == nil) // falseMaps are Go's built-in hash table. Keys must be comparable types (those that support == and !=). Maps are reference types β like slices, they are cheap to pass because only a header is copied.
// Creating maps
m := map[string]int{} // empty map literal (ready to use)
m2 := make(map[string]int) // same β make preferred when initial size is known
m3 := map[string]int{ // initialised map
"alice": 30, "bob": 25,
}
// Insert / update
m3["carol"] = 28
m3["alice"] = 31 // update β no error if key exists
// Read β returns zero value for missing keys, NOT an error
age := m3["alice"] // 31
missing := m3["dave"] // 0 β zero value for int
// Comma-ok idiom β check if key actually exists
age, ok := m3["alice"]
if ok {
fmt.Printf("alice is %d\n", age)
}
// Delete
delete(m3, "bob")
// Iteration β ORDER IS NOT GUARANTEED
for name, age := range m3 {
fmt.Printf("%s: %d\n", name, age)
}
// PITFALL: reading nil map is OK (returns zero value)
var bad map[string]int
_ = bad["key"] // safe β returns 0
// bad["key"] = 1 // PANIC: assignment to entry in nil mapStructs are Go's primary mechanism for grouping related data. Methods are functions with a receiver β the type they are attached to. The receiver can be a value or a pointer, which changes whether the method can modify the struct.
type Person struct {
FirstName string
LastName string
Age int
email string // unexported
}
// Creating struct instances
p1 := Person{FirstName: "Alice", LastName: "Smith", Age: 30}
p2 := &Person{FirstName: "Bob", Age: 25} // pointer to struct
// Value receiver β works on a copy; cannot modify the original
func (p Person) FullName() string {
return p.FirstName + " " + p.LastName
}
// Pointer receiver β modifies the original struct
func (p *Person) HaveBirthday() {
p.Age++
}
fmt.Println(p1.FullName()) // Alice Smith
p1.HaveBirthday() // Go auto-takes address: (&p1).HaveBirthday()
fmt.Println(p1.Age) // 31
// Anonymous struct β useful for one-off data grouping
point := struct{ X, Y int }{X: 3, Y: 7}
// Struct embedding β promotes fields and methods (composition)
type Employee struct {
Person // embedded β promotes Name, HaveBirthday, etc.
Company string
Salary float64
}
e := Employee{Person: Person{FirstName: "Carol", Age: 28}, Company: "Acme"}
fmt.Println(e.FullName()) // Carol β promoted methodAn interface specifies a set of method signatures. Any type that implements all the methods satisfies the interface β implicitly, with no declaration. This is sometimes called structural typing or duck typing with static checking.
// Interface definition
type Shape interface {
Area() float64
Perimeter() float64
}
// Circle satisfies Shape β no 'implements' keyword needed
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64 { return r.W * r.H }
func (r Rectangle) Perimeter() float64 { return 2 * (r.W + r.H) }
// Polymorphic function β accepts any Shape
func printShape(s Shape) {
fmt.Printf("Area=%.2f Perimeter=%.2f\n", s.Area(), s.Perimeter())
}
shapes := []Shape{Circle{5}, Rectangle{4, 6}}
for _, s := range shapes { printShape(s) }
// Type assertion β extract concrete type (safe form)
var s Shape = Circle{Radius: 3}
c, ok := s.(Circle)
if ok { fmt.Println("radius:", c.Radius) } // radius: 3
// Type switch β check and handle multiple types
switch v := s.(type) {
case Circle: fmt.Printf("circle r=%.1f\n", v.Radius)
case Rectangle: fmt.Printf("rect %.0fx%.0f\n", v.W, v.H)
default: fmt.Println("unknown shape")
}
// Compile-time interface check (zero-cost assertion)
var _ Shape = Circle{} // COMPILE ERROR if Circle doesn't implement ShapeAn interface with zero methods is satisfied by every type in Go. Written as interface{} or its alias any (Go 1.18+), it lets a variable or function parameter hold a value of any type. It is Go's mechanism for truly generic containers β at the cost of compile-time type safety.
// any is an alias for interface{} (Go 1.18+)
func describe(v any) {
fmt.Printf("Type: %-10T Value: %v\n", v, v)
}
describe(42) // Type: int Value: 42
describe("hello") // Type: string Value: hello
describe([]int{1, 2, 3}) // Type: []int Value: [1 2 3]
// Heterogeneous collection
row := []any{1, "Alice", true, 3.14}
// Must type-assert to get the concrete value back
var v any = "hello"
s, ok := v.(string) // safe β ok=false if not a string
// s2 := v.(int) // panics if v is not an int
// Type switch β handle multiple possible types
for _, item := range row {
switch x := item.(type) {
case int: fmt.Println("int:", x)
case string: fmt.Println("string:", x)
case bool: fmt.Println("bool:", x)
default: fmt.Println("other:", x)
}
}
// PREFER: narrow interfaces, specific types, or generics over 'any'
// 'any' loses compile-time type safety β errors become runtime panicsGo has pointers β variables that store the memory address of another variable β but removes the dangerous parts of C pointers. There is no pointer arithmetic, no manual memory management, and the garbage collector handles deallocation. A pointer to a local variable is safe to return from a function.
x := 42
p := &x // & gives the address of x; p is *int
fmt.Println(*p) // 42 β * dereferences: gives the value at the address
*p = 100 // modify x through the pointer
fmt.Println(x) // 100
// new() β allocates a zeroed value and returns its pointer
q := new(int) // *int pointing to 0
*q = 55
// nil pointer β the zero value for any pointer type
var ptr *int
fmt.Println(ptr) //
// fmt.Println(*ptr) // PANIC: nil pointer dereference
// Passing by pointer β allows a function to modify the caller's variable
func increment(n *int) { *n++ }
val := 10
increment(&val)
fmt.Println(val) // 11
// SAFE: returning pointer to local β Go's escape analysis handles this
type Point struct{ X, Y int }
func newPoint(x, y int) *Point {
p := Point{x, y} // may be allocated on heap by compiler
return &p // safe in Go; would be dangling pointer in C!
}
// Auto-dereference on struct pointers β no -> operator needed
pp := &Point{1, 2}
pp.X = 10 // same as (*pp).X = 10 Go treats errors as values returned by functions, not as exceptions thrown from the call stack. This makes error handling explicit and visible. Every function that can fail returns an error as its last return value. The caller is responsible for handling it.
// error is a built-in interface: type error interface { Error() string }
// Standard pattern
func parseAge(s string) (int, error) {
age, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parseAge: %w", err) // %w wraps the error
}
if age < 0 || age > 150 {
return 0, fmt.Errorf("parseAge: invalid age %d", age) // %v or plain
}
return age, nil
}
// Sentinel errors β compare with errors.Is()
var ErrNotFound = errors.New("not found")
// %w wraps the error β errors.Is / errors.As can inspect the chain
wrapped := fmt.Errorf("lookupUser: %w", ErrNotFound)
fmt.Println(errors.Is(wrapped, ErrNotFound)) // true
// %v formats as string β errors.Is CANNOT find original error
notWrapped := fmt.Errorf("lookupUser: %v", ErrNotFound)
fmt.Println(errors.Is(notWrapped, ErrNotFound)) // false!
// Custom error type
type ValidationError struct{ Field, Msg string }
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
// errors.As β extract a specific error type from the chain
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("bad field:", ve.Field)
}A goroutine is a lightweight, concurrently executing function managed by the Go runtime. The cost to create one is ~2 KB of stack and ~300 ns β roughly 1000Γ cheaper than an OS thread. The runtime multiplexes goroutines onto OS threads with its own scheduler.
// Launch a goroutine with the 'go' keyword
go fmt.Println("running concurrently")
// Anonymous goroutine
go func(msg string) {
fmt.Println(msg)
}("hello from goroutine")
// PROBLEM: main() may return before goroutines finish
// SOLUTION: sync.WaitGroup
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // register ONE more goroutine β do this BEFORE go
go func(n int) {
defer wg.Done() // signal this goroutine is complete
fmt.Printf("worker %d\n", n)
}(i)
}
wg.Wait() // block until all goroutines call Done() β counter reaches 0
fmt.Println("all workers finished")
// Output (order may vary):
// worker 3
// worker 1
// worker 0
// worker 4
// worker 2
// all workers finishedChannels are typed conduits for sending values between goroutines. They are goroutine-safe and provide the synchronisation primitive underlying Go's concurrency model. Go's philosophy: "Do not communicate by sharing memory; instead, share memory by communicating."
// Unbuffered channel β send BLOCKS until a receiver is ready
ch := make(chan int)
go func() { ch <- 42 }() // goroutine parks until main receives
v := <-ch // unblocks sender
fmt.Println(v) // 42
// Buffered channel β send blocks only when buffer is FULL
buf := make(chan string, 3)
buf <- "a" // no goroutine needed β goes into buffer
buf <- "b"
fmt.Println(<-buf) // a (FIFO)
fmt.Println(<-buf) // b
// Closing a channel signals: no more values will be sent
jobs := make(chan int, 5)
for i := 0; i < 5; i++ { jobs <- i }
close(jobs)
// Range β exits automatically when channel is closed and drained
for j := range jobs { fmt.Println(j) } // 0 1 2 3 4
// Comma-ok: detect closed channel
val, ok := <-jobs
fmt.Println(val, ok) // 0 false
// select β wait on multiple channel operations
select {
case v := <-ch1: fmt.Println("ch1:", v)
case v := <-ch2: fmt.Println("ch2:", v)
case <-time.After(time.Second): fmt.Println("timeout")
}defer schedules a function call to run when the surrounding function returns β regardless of how it returns (normally, via error, or via panic). It is Go's idiomatic resource-cleanup mechanism. panic stops normal execution; recover catches it inside a deferred function.
// defer β executes when surrounding function returns
func processFile(path string) error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close() // ALWAYS runs, even if function returns early
// process file...
return nil
}
// Multiple defers: LIFO order (last-in, first-out)
func demo() {
defer fmt.Println("3rd") // runs first (LIFO)
defer fmt.Println("2nd")
defer fmt.Println("1st") // runs last? NO β runs first!
// Output order: 1st, 2nd, 3rd β wait, no:
// ACTUAL order: 3rd β 2nd β 1st (LIFO!)
}
// Defer argument evaluation: immediate, not deferred!
x := 5
defer fmt.Println(x) // prints 5 β x evaluated NOW
x = 10 // too late to affect the defer
// panic β signals an unrecoverable error
func mustPositive(n int) int {
if n <= 0 { panic(fmt.Sprintf("expected positive, got %d", n)) }
return n
}
// recover β catches a panic; ONLY useful inside defer
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil // panics if b == 0
}A closure is a function that references and closes over variables from its surrounding scope. The closure captures the variable itself β not a copy of its value at the moment of creation. This distinction is the source of one of the most common Go bugs.
// Closure capturing outer variable
func makeCounter() func() int {
count := 0
return func() int {
count++ // captures 'count' β reads its current value each call
return count
}
}
c := makeCounter()
fmt.Println(c(), c(), c()) // 1 2 3
c2 := makeCounter() // independent counter
fmt.Println(c2()) // 1 (fresh)
// CLASSIC GOTCHA: loop variable capture
funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
funcs[i] = func() { fmt.Println(i) } // captures &i, not a copy!
}
funcs[0]() // prints 3 (not 0!)
funcs[1]() // prints 3
funcs[2]() // prints 3
// By the time any func runs, the loop ended and i == 3
// FIX 1: pass i as an argument (creates a per-iteration copy)
for i := 0; i < 3; i++ {
go func(n int) { fmt.Println(n) }(i) // n is a copy of i
}
// FIX 2: shadow i with a new variable per iteration
for i := 0; i < 3; i++ {
i := i // new 'i' that belongs to this iteration
funcs[i] = func() { fmt.Println(i) }
}
// Go 1.22+: loop variables are per-iteration by default β bug gone!init() is an optional special function that runs automatically after all package-level variable initialisations β before main(). It takes no arguments, returns nothing, and cannot be called directly. A single file may contain multiple init() functions.
package main
import (
"fmt"
_ "github.com/lib/pq" // blank import: run pq's init() for side effects
) // registers the postgres driver
var cfg *Config
func init() {
// Runs BEFORE main(), AFTER package-level vars are initialised
var err error
cfg, err = loadConfig("app.yaml")
if err != nil {
panic(fmt.Sprintf("config init failed: %v", err))
}
fmt.Println("config loaded")
}
func main() {
fmt.Println("main running")
// cfg is guaranteed to be non-nil here
}
// INITIALISATION ORDER within a package:
// 1. Package-level variables (in dependency order)
// 2. init() functions (in source file order, multiple per file allowed)
// 3. main() β only in package main
// Across packages: imported packages initialise first
// Go guarantees no circular init dependenciesModules are Go's dependency management system (stable since Go 1.13). A module is a collection of related packages identified by a module path (typically a repository URL). The module's root contains a go.mod file that records the module path, the minimum Go version, and all required dependencies.
// Initialise a module
// $ go mod init github.com/alice/myapp
// go.mod β human-editable; commit to version control
// βββββββββββββββββββββββββββββββββββββββββββββββββ
// module github.com/alice/myapp
//
// go 1.22
//
// require (
// github.com/gin-gonic/gin v1.9.1
// golang.org/x/sync v0.6.0
// )
// go.sum β machine-managed; commit to version control
// Contains SHA-256 hashes of each downloaded module zip
// Guarantees tamper-proof, reproducible builds
// Key commands:
// go get github.com/pkg@v1.2.3 β add / upgrade dependency
// go mod tidy β remove unused, add missing deps
// go mod download β pre-download all deps
// go list -m all β list entire dependency graph
// Minimum Version Selection (MVS):
// Go ALWAYS picks the minimum version that satisfies all requirements.
// Nothing upgrades silently β builds are reproducible.
// Major version imports (breaking changes need new import path):
// import "github.com/alice/pkg/v2" β v2 has breaking API changesA data race occurs when two or more goroutines access the same memory location concurrently, at least one access is a write, and there is no synchronisation between them. Data races produce undefined, non-deterministic behaviour β results vary between runs and can silently corrupt data.
// DATA RACE β unsafe counter increment from multiple goroutines
var counter int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // READ + INCREMENT + WRITE β not atomic!
}()
}
wg.Wait()
fmt.Println(counter) // less than 1000 β data was lost!
// DETECT: go run -race main.go or go test -race ./...
// Race detector output:
// ==================
// WARNING: DATA RACE
// Write at 0x... by goroutine 8: main.main.func1() :12
// Previous write at 0x... by goroutine 7: main.main.func1() :12
// ==================
// FIX 1: sync.Mutex
var mu sync.Mutex
go func() { mu.Lock(); counter++; mu.Unlock() }()
// FIX 2: atomic operation (faster for simple numeric ops)
var atomicCounter int64
go func() { atomic.AddInt64(&atomicCounter, 1) }()
// FIX 3: channel β one goroutine owns the counter
inc := make(chan struct{}, 100)
go func() { n := 0; for range inc { n++ } }()
inc <- struct{}{} // safeThe fmt package defines the Stringer interface: a type that implements String() string controls how it appears when printed with fmt.Println, fmt.Printf("%v"), and related functions. Implementing error works the same way for error messages.
// fmt.Stringer interface:
// type Stringer interface { String() string }
type Direction int
const (
North Direction = iota
South; East; West
)
func (d Direction) String() string {
return [...]string{"North", "South", "East", "West"}[d]
}
fmt.Println(North) // North (not: 0)
fmt.Printf("%v\n", East) // East
fmt.Printf("%s\n", West) // West
// Another example: Point with custom format
type Point struct{ X, Y float64 }
func (p Point) String() string {
return fmt.Sprintf("(%.1f, %.1f)", p.X, p.Y)
}
p := Point{3.5, 7.2}
fmt.Println(p) // (3.5, 7.2)
fmt.Printf("%v\n", p) // (3.5, 7.2)
// TRAP: infinite recursion inside String()
// func (p Point) String() string {
// return fmt.Sprintf("%v", p) // calls String() again β stack overflow!
// }
// FIX: cast to a non-Stringer type
// return fmt.Sprintf("%v", struct{ X, Y float64 }(p))Go has two ways to give a new name to a type, with importantly different semantics. Understanding this prevents subtle type-safety bugs and confusing compiler errors.
| Aspect | Type Definition: type T U | Type Alias: type T = U |
|---|---|---|
| Creates new type? | YES β T and U are distinct types | NO β T is just another name for U |
| Methods of U inherited? | No β T starts with no methods | Yes β T and U are identical |
| T β U assignment | Requires explicit conversion: U(t) | No conversion needed |
| Use for | Adding type safety, defining method sets | Code migration, readability aliases |
// TYPE DEFINITION β new type with type safety
type Celsius float64
type Fahrenheit float64
func (c Celsius) ToFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
var bodyTemp Celsius = 37.0
// var t Fahrenheit = bodyTemp // COMPILE ERROR β different types!
var t Fahrenheit = bodyTemp.ToFahrenheit() // OK
// Prevents bugs: you cannot accidentally mix temperatures
func setOven(temp Celsius) { /* ... */ }
// setOven(Fahrenheit(350)) // COMPILE ERROR β type safety!
setOven(Celsius(180)) // OK
// TYPE ALIAS β same type, different name
type MyString = string
var s1 string = "hello"
var s2 MyString = s1 // OK β same type
s1 = s2 // OK β no conversion
// Built-in aliases you already use:
// byte = uint8
// rune = int32
// any = interface{}Go strings are immutable sequences of bytes stored in UTF-8 encoding. Since UTF-8 is a variable-width encoding, a single character (rune) can occupy 1 to 4 bytes. This means len(s) reports bytes, not characters β a fact that trips up many beginners.
s := "Hello, δΈη" // UTF-8 string containing ASCII + Chinese chars
// len() counts BYTES
fmt.Println(len(s)) // 13
// 7 ASCII (1 byte each) + 2 Chinese (3 bytes each) = 13
// Count RUNES (Unicode characters)
fmt.Println(utf8.RuneCountInString(s)) // 9
fmt.Println(len([]rune(s))) // 9 (same)
// Byte-by-byte iteration β WRONG for multi-byte chars
for i := 0; i < len(s); i++ {
fmt.Printf("%d:%x ", i, s[i]) // raw byte values
}
// Rune-by-rune iteration β CORRECT for Unicode
// range decodes UTF-8 runes automatically
for i, r := range s { // i = byte offset, r = rune (Unicode code point)
fmt.Printf("%d:%c ", i, r)
}
// Conversions
b := []byte(s) // string β []byte (copies)
r := []rune(s) // string β []rune (copies)
s2 := string(b) // []byte β string
s3 := string(r) // []rune β string
// Efficient string building (avoid + in loops)
var sb strings.Builder
for _, word := range []string{"Go", " ", "rocks"} {
sb.WriteString(word)
}
fmt.Println(sb.String()) // Go rocksA goroutine leak occurs when a goroutine is started but never terminates β it stays blocked forever waiting on a channel, mutex, or network call that will never complete. Goroutines are cheap but not free: leaked goroutines accumulate over time and eventually exhaust memory in long-running services.
// LEAK β goroutine blocks forever; nobody ever sends to ch
func leaky() {
ch := make(chan int)
go func() {
v := <-ch // blocks indefinitely
fmt.Println(v)
}()
// function returns β goroutine is stuck forever!
}
// FIX: pass a context and select on ctx.Done()
func safe(ctx context.Context, ch <-chan int) {
go func() {
select {
case v := <-ch:
fmt.Println(v)
case <-ctx.Done(): // goroutine exits cleanly when cancelled
return
}
}()
}
// FIX: close the channel to unblock receivers
func producer() <-chan int {
ch := make(chan int)
go func() {
defer close(ch) // close signals: no more values
for _, v := range data {
ch <- v
}
}()
return ch
}
// Detect leaks in tests:
// defer goleak.VerifyNone(t) β fails test if goroutines remain
// runtime.NumGoroutine() β watch for steady growthsync.Mutex provides mutual exclusion β at most one goroutine holds the lock at any moment. sync.RWMutex is an extension: multiple goroutines can hold a read lock simultaneously, but a write lock is exclusive. Use RWMutex when reads vastly outnumber writes.
// sync.Mutex β protect any shared mutable state
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Add(n int) {
c.mu.Lock()
defer c.mu.Unlock() // always use defer β runs even on panic
c.count += n
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// sync.RWMutex β read-heavy workloads (e.g. config, caches)
type SafeConfig struct {
mu sync.RWMutex
data map[string]string
}
func (c *SafeConfig) Get(key string) string {
c.mu.RLock() // multiple goroutines can hold RLock at once
defer c.mu.RUnlock()
return c.data[key]
}
func (c *SafeConfig) Set(key, val string) {
c.mu.Lock() // exclusive β no readers OR writers allowed
defer c.mu.Unlock()
c.data[key] = val
}
// RULES:
// 1. Never copy a Mutex after first use (lock state would be duplicated)
// 2. Always pass struct containing Mutex as a pointer (*SafeCounter)
// 3. Mutex is NOT reentrant β a goroutine holding Lock() will deadlock
// if it calls Lock() again on the same mutexGo uses composition through embedding rather than class inheritance. When a type is embedded (without a field name), its exported methods and fields are promoted to the outer type β they are directly accessible. However, the outer type is NOT a subtype of the embedded type.
type Logger struct{ prefix string }
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
// Server embeds Logger β gains the Log method
type Server struct {
*Logger // embedded pointer (promotes Log)
host string
port int
}
srv := Server{
Logger: &Logger{prefix: "SERVER"},
host: "localhost",
port: 8080,
}
srv.Log("starting") // SERVER: starting β promoted method!
srv.Logger.Log("starting") // same thing, explicit call
// Method override β Server can define its own Log
func (s *Server) Log(msg string) {
s.Logger.Log(fmt.Sprintf("%s:%d β %s", s.host, s.port, msg))
}
// KEY: embedding is NOT inheritance
type Describer interface{ Describe() string }
type Base struct{}
func (Base) Describe() string { return "I am Base" }
type Derived struct{ Base }
// Derived satisfies Describer via promotion:
var d Describer = Derived{} // works!
// BUT: you CANNOT use Derived where Base is required
func process(b Base) {}
// process(Derived{}) // COMPILE ERROR β not a subtype!append adds elements to a slice. When there is spare capacity in the backing array, it writes directly into it and increments the length β no allocation. When capacity is exhausted, it allocates a new, larger array, copies all elements, and returns a new slice header. This is why you must always assign the return value of append.
s := make([]int, 3, 5) // len=3, cap=5, backing array has 2 spare slots
fmt.Println(len(s), cap(s)) // 3 5
s = append(s, 10) // len=4, cap=5 β no allocation (has room)
s = append(s, 20) // len=5, cap=5 β no allocation (fills last slot)
s = append(s, 30) // len=6, cap>=10 β REALLOCATES! new backing array
fmt.Println(len(s), cap(s)) // 6 10 (capacity grew to ~2x)
// Sub-slice shares backing array β subtle bug territory
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2 3] β shares backing array with a
fmt.Println(cap(b)) // 4 (from position 1 to end of a's array)
b = append(b, 99) // writes to a[3]! No reallocation needed
fmt.Println(a) // [1 2 3 99 5] β a was modified!
// FIX: use copy to get an independent slice
b2 := make([]int, len(a[1:3]))
copy(b2, a[1:3]) // independent backing array
b2 = append(b2, 99) // safe β doesn't affect a
// Pre-allocate when final length is known β avoids repeated reallocation
result := make([]int, 0, len(input)) // cap=len(input), no mid-loop alloc
for _, v := range input {
result = append(result, v*2)
}context.Context is Go's standard mechanism for propagating three things across API boundaries and goroutine calls: cancellation signals, deadlines, and request-scoped values. Passing it as the first parameter is a Go convention β it allows any blocking call to be cancelled.
// The context.Context interface:
// Done() <-chan struct{} β closed when cancelled or deadline passed
// Err() error β nil, Canceled, or DeadlineExceeded
// Deadline() (time.Time, bool)
// Value(key) any
// Root contexts
ctx := context.Background() // top-level: no deadline, no cancel
ctx = context.TODO() // placeholder when ctx not yet determined
// Derived contexts β ALWAYS defer cancel()
ctx1, cancel1 := context.WithCancel(context.Background())
defer cancel1() // prevents goroutine leak inside context machinery
ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel2()
// Use in a worker goroutine
func fetchData(ctx context.Context, url string) ([]byte, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err } // err contains context.DeadlineExceeded
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// Checking cancellation in a loop
func processItems(ctx context.Context, items []Item) error {
for _, item := range items {
select {
case <-ctx.Done(): // cancelled β stop early
return ctx.Err()
default: // continue
}
process(item)
}
return nil
}An interface value in Go has two components: a dynamic type and a dynamic value. An interface is nil only when BOTH are nil. A common mistake is returning a typed nil pointer as an error β the interface has a type component set, so it is NOT nil even though the pointer value is nil.
// The trap: returning a *MyError that is nil as an error interface
type MyError struct{ Code int }
func (e *MyError) Error() string { return fmt.Sprintf("error %d", e.Code) }
func riskyOperation(succeed bool) error {
var err *MyError // err is a nil *MyError pointer
if !succeed {
err = &MyError{Code: 404}
}
return err // BUG: returns interface{type=*MyError, value=nil}
// This interface is NOT nil!
}
result := riskyOperation(true) // supposed to succeed
if result != nil { // UNEXPECTED: true! Interface is non-nil.
fmt.Println("error:", result) // prints: error 0
}
// FIX: return bare nil, not a typed nil pointer
func safeOperation(succeed bool) error {
if !succeed {
return &MyError{Code: 404}
}
return nil // correct: returns a nil interface, not a *MyError nil
}
// Verify the fix
result2 := safeOperation(true)
fmt.Println(result2 == nil) // true β correct!
// Rule: functions returning an error interface should NEVER return
// a concrete typed pointer that might be nil. Always return bare nil.Knowing fmt format verbs lets you produce clear output for debugging, logging, and user messages. The %v verb is the universal default; specialised verbs give more control.
| Verb | Meaning | Example output |
|---|---|---|
| %v | Default format for any value | 42, true, [1 2 3] |
| %+v | Struct with field names | {Name:Alice Age:30} |
| %#v | Go-syntax representation | main.Person{Name:"Alice", Age:30} |
| %T | Type of the value | int, []string, main.Person |
| %d | Integer in decimal | 42 |
| %f / %.2f | Float / float with 2 decimal places | 3.141590 / 3.14 |
| %s | Plain string | hello |
| %q | Quoted string | "hello" |
| %x | Hex encoding | ff (int) or 68656c6c6f (string) |
| %p | Pointer address | 0xc000012080 |
| %w | Wrap an error (Errorf only) | used for error chaining |
name := "Alice"
age := 30
pi := 3.14159
fmt.Printf("%s is %d years old\n", name, age) // Alice is 30 years old
fmt.Printf("Pi β %.2f\n", pi) // Pi β 3.14
fmt.Printf("Type: %T\n", pi) // Type: float64
// Sprintf β format to string, no output
msg := fmt.Sprintf("User: %s (age %d)", name, age)
// Errorf β format and create an error
err := fmt.Errorf("user %q not found (id: %d)", name, 99)
// Width and padding
fmt.Printf("%10s\n", "right") // right (right-align, width 10)
fmt.Printf("%-10s|\n", "left") // left | (left-align, width 10)
fmt.Printf("%06d\n", 42) // 000042 (zero-pad to width 6)
// Printing structs
type Person struct{ Name string; Age int }
p := Person{"Bob", 25}
fmt.Printf("%v\n", p) // {Bob 25}
fmt.Printf("%+v\n", p) // {Name:Bob Age:25}
fmt.Printf("%#v\n", p) // main.Person{Name:"Bob", Age:25}