Mastering GoLang: The Complete Guide to Syntax, Pointers & Data Structures
From 'Hello World' to complex memory management: a comprehensive engineering deep dive into Go's core syntax, type system, and internal data structures.
Go Basics
Go installation and setup
Go installation involves downloading the binary from golang.org, extracting it to /usr/local/go (Linux/Mac) or C:\Go (Windows), and adding the bin directory to your PATH environment variable. Verify with go version.
# Linux/Mac wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz export PATH=$PATH:/usr/local/go/bin
GOPATH and GOROOT
GOROOT is where Go is installed (the SDK location), while GOPATH is your workspace for Go projects, dependencies, and binaries—defaulting to $HOME/go. With Go modules (1.11+), GOPATH is less critical but still used for go install binaries.
GOROOT=/usr/local/go # Go SDK installation GOPATH=$HOME/go # Your workspace ├── bin/ # Installed binaries ├── pkg/ # Cached packages └── src/ # Source code (legacy)
Go workspace structure
Modern Go uses modules (go.mod) allowing projects anywhere on disk. Each module contains packages in directories, with one package per directory and all files in a directory belonging to the same package.
myproject/ ├── go.mod # Module definition ├── go.sum # Dependency checksums ├── main.go # Main package ├── internal/ # Private packages │ └── auth/ │ └── auth.go └── pkg/ # Public packages └── utils/ └── utils.go
go command basics
The go command is the Swiss Army knife for building, testing, and managing Go code—it handles compilation, dependency management, testing, formatting, and more without needing external build tools like Make.
go build # Compile packages and dependencies go run # Compile and run Go program go test # Run tests go mod init # Initialize new module go mod tidy # Add missing, remove unused deps go get # Add dependencies go fmt # Format source code go vet # Report suspicious constructs
Hello World program
Every Go program starts with a package declaration, imports needed packages, and for executables, must have a main package with a main function as the entry point.
package main import "fmt" func main() { fmt.Println("Hello, World!") }
Package declaration
The package declaration is the first line in every Go file, defining which package the file belongs to. Executable programs use package main, while libraries use descriptive names matching their directory.
package main // Executable program package http // Library package (in http/ directory) package http_test // External test package
import statement
Import statements bring other packages into scope, with the package path in quotes. You can import multiple packages with parentheses, alias imports, or use blank identifier for side-effects only.
import "fmt" // Single import import ( // Multiple imports "fmt" "strings" "github.com/pkg/errors" // External package str "strings" // Aliased import . "math" // Dot import (avoid) _ "image/png" // Side-effect only )
Exported vs unexported names
Go uses capitalization for visibility: names starting with uppercase are exported (public), accessible from other packages; lowercase names are unexported (private), only visible within the same package.
package user type User struct { // Exported - visible outside package Name string // Exported field age int // unexported - package private } func NewUser() *User { } // Exported function func validate() bool { } // unexported function
Variables declaration (var, :=)
Variables can be declared with var (explicit, can be at package level) or := (short declaration, type inferred, only inside functions). The short form is idiomatic for local variables.
var name string // Zero value: "" var age int = 25 // Explicit type and value var city = "NYC" // Type inferred func main() { count := 10 // Short declaration (functions only) x, y := 1, 2 // Multiple variables var ( // Block declaration a = 1 b = 2 ) }
Zero values
Go automatically initializes variables to their zero value when not explicitly set—no uninitialized memory. This is a safety feature ensuring predictable behavior.
var i int // 0 var f float64 // 0.0 var b bool // false var s string // "" (empty string) var p *int // nil var sl []int // nil var m map[int]int // nil var ch chan int // nil var fn func() // nil var iface error // nil
Type inference
Go infers types from the right-hand side of assignments, making code concise while maintaining static typing. The inferred type matches the literal's default type or the expression's result type.
x := 42 // int (default for integer literals) y := 3.14 // float64 (default for float literals) z := "hello" // string c := 'A' // rune (alias for int32) b := true // bool // Expression inference sum := x + 10 // int (same as x) result := math.Sqrt(2.0) // float64 (function return type)
Constants (const)
Constants are immutable values known at compile time, declared with const. They can be untyped (more flexible in expressions) or typed, and cannot be declared with :=.
const Pi = 3.14159 // Untyped constant const MaxSize int = 100 // Typed constant const ( // Block declaration StatusOK = 200 StatusNotFound = 404 ) const ( KB = 1024 MB = KB * 1024 // Compile-time expression GB = MB * 1024 )
iota
iota is a constant generator that starts at 0 and increments for each constant in a block—perfect for enumerations. It resets to 0 in each new const block.
const ( Sunday = iota // 0 Monday // 1 Tuesday // 2 Wednesday // 3 ) const ( _ = iota // 0 (skip) KB = 1 << (10 * iota) // 1 << 10 = 1024 MB // 1 << 20 = 1048576 GB // 1 << 30 ) const ( Read = 1 << iota // 1 Write // 2 Execute // 4 )
Basic data types (int, float, bool, string)
Go has sized numeric types for precise control and platform-dependent int/uint. Strings are immutable UTF-8 byte sequences, and bool is strictly true/false (no truthy/falsy).
┌─────────────────────────────────────────────────────┐ │ INTEGERS │ │ int8, int16, int32, int64 (signed) │ │ uint8, uint16, uint32, uint64 (unsigned) │ │ int, uint (platform: 32/64) │ │ byte (alias uint8), rune (alias int32) │ ├─────────────────────────────────────────────────────┤ │ FLOATS: float32, float64 │ │ COMPLEX: complex64, complex128 │ │ BOOL: true, false │ │ STRING: immutable UTF-8 bytes │ └─────────────────────────────────────────────────────┘
Type conversion
Go requires explicit type conversions—no implicit coercion between types, even between int and int64. Use T(value) syntax for conversions; some may truncate or lose precision.
var i int = 42 var f float64 = float64(i) // int to float64 var u uint = uint(f) // float64 to uint // String conversions (strconv package) s := strconv.Itoa(42) // int to string: "42" n, _ := strconv.Atoi("42") // string to int: 42 // Byte slice <-> string bytes := []byte("hello") str := string(bytes)
Operators (arithmetic, comparison, logical, bitwise)
Go provides standard operators with no operator overloading. Arithmetic operators work on numeric types, comparison operators return bool, and bitwise operators work on integers.
┌───────────────────────────────────────────────────┐ │ Arithmetic: + - * / % │ │ Comparison: == != < > <= >= │ │ Logical: && || ! │ │ Bitwise: & | ^ &^ << >> │ │ Assignment: = += -= *= /= %= &= |= ^= │ │ Other: &(address) *(deref) <-(channel) │ └───────────────────────────────────────────────────┘
// Bitwise example flags := 0b1010 flags |= 0b0001 // Set bit: 1011 flags &^= 0b0010 // Clear bit: 1001
Comments (single-line, multi-line)
Single-line comments use //, multi-line use /* */. Doc comments immediately preceding declarations become documentation accessible via go doc and pkg.go.dev.
// This is a single-line comment /* This is a multi-line comment spanning multiple lines */ // Package http provides HTTP client and server implementations. // This is a doc comment - it documents the package. package http // Get issues a GET request to the specified URL. // Doc comments should be complete sentences. func Get(url string) (*Response, error) { }
Semicolons (automatic insertion)
Go's lexer automatically inserts semicolons at line ends after tokens that could end a statement—this is why opening braces must be on the same line as if, for, func, etc.
// These are equivalent: func main() { fmt.Println("hello") } func main() { fmt.Println("hello"); } // This FAILS - semicolon inserted after ) func main() // ; inserted here! { // syntax error } // Correct - brace on same line func main() { }
gofmt and code formatting
gofmt is the standard formatter that enforces consistent style—tabs for indentation, spaces for alignment. Run it automatically with editor integration or go fmt ./.... There are no style debates in Go.
gofmt -w main.go # Format and write back gofmt -d main.go # Show diff go fmt ./... # Format entire module # goimports = gofmt + import management goimports -w main.go
go run vs go build
go run compiles to a temp directory and executes immediately—great for development. go build creates a binary in the current directory (or specified output) for distribution.
go run main.go # Compile and run (temp binary) go run . # Run package in current dir go build # Create binary: ./myapp go build -o mybin # Custom output name go build -ldflags="-s -w" # Strip debug info (smaller) # Cross-compilation GOOS=linux GOARCH=amd64 go build -o myapp-linux GOOS=windows GOARCH=amd64 go build -o myapp.exe
go install
go install compiles and installs the binary to $GOPATH/bin (or $GOBIN), making it available in your PATH. It's the standard way to install Go tools and your own commands.
# Install a tool from a module go install golang.org/x/tools/gopls@latest # Install your local command go install ./cmd/myapp # Result ls $GOPATH/bin/ # gopls myapp # Now runnable from anywhere myapp --help
Control Structures
if statement
Go's if requires no parentheses around conditions but always requires braces. The condition must be a boolean expression—no truthy/falsy values like in other languages.
if x > 10 { fmt.Println("x is large") } if user != nil && user.IsActive { process(user) }
if with short statement
Go allows a short statement before the condition, separated by semicolon. Variables declared in this statement are scoped to the if-else block—perfect for error handling.
if err := doSomething(); err != nil { return err // err is scoped to this if-else block } // err not accessible here if n, err := fmt.Println("hi"); err != nil { log.Fatal(err) } else { fmt.Printf("wrote %d bytes\n", n) }
else and else if
else and else if chain conditionals together. The else must be on the same line as the closing brace (due to semicolon insertion). Variables from the if-statement are accessible in all branches.
if score >= 90 { grade = "A" } else if score >= 80 { grade = "B" } else if score >= 70 { grade = "C" } else { grade = "F" }
for loop (only loop in Go)
Go has only for—no while, do-while, or foreach. It's versatile: three-component (init;condition;post), condition-only (like while), or infinite. All parts are optional.
// Traditional for for i := 0; i < 10; i++ { fmt.Println(i) } // Multiple variables for i, j := 0, 10; i < j; i, j = i+1, j-1 { fmt.Println(i, j) }
for as while
Omit the init and post statements to get while-loop behavior. This is idiomatic Go for condition-based loops.
// While-style n := 0 for n < 5 { fmt.Println(n) n++ } // Read until EOF for scanner.Scan() { fmt.Println(scanner.Text()) }
Infinite loops
Omit all three components for an infinite loop—commonly used for servers, event loops, or loops with complex exit conditions handled by break/return.
for { conn, err := listener.Accept() if err != nil { break } go handle(conn) } // Or with return for { if done() { return } doWork() }
Range-based loops
range iterates over slices, arrays, maps, strings, and channels, returning index/value pairs. Use _ to discard unwanted values.
// Slice: index, value for i, v := range []string{"a", "b", "c"} { fmt.Println(i, v) // 0 a, 1 b, 2 c } // Map: key, value (random order!) for k, v := range map[string]int{"a": 1, "b": 2} { fmt.Println(k, v) } // String: index, rune (not byte!) for i, r := range "Go日本" { fmt.Printf("%d: %c\n", i, r) // 0:G 1:o 2:日 5:本 } // Channel: value only for msg := range ch { process(msg) } // Index only for i := range slice { slice[i] = 0 // Modify in place }
break statement
break exits the innermost for, switch, or select. Use labeled break to exit outer loops from nested loops.
// Simple break for { if done { break } } // Labeled break (exit outer loop) outer: for i := 0; i < 10; i++ { for j := 0; j < 10; j++ { if i*j > 50 { break outer // Breaks outer loop } } }
continue statement
continue skips to the next iteration of the innermost loop. Like break, it supports labels for outer loops.
for i := 0; i < 10; i++ { if i%2 == 0 { continue // Skip even numbers } fmt.Println(i) // 1, 3, 5, 7, 9 } // Labeled continue outer: for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { if j == 1 { continue outer // Next i iteration } } }
switch statement
Go's switch is cleaner than C's: no automatic fallthrough, cases can be expressions, and cases break automatically. No parentheses needed around the expression.
switch day { case "Monday": fmt.Println("Start of week") case "Friday": fmt.Println("TGIF!") case "Saturday", "Sunday": // Multiple values fmt.Println("Weekend!") default: fmt.Println("Midweek") }
switch with no condition
A switch without an expression is switch true—a clean way to write long if-else chains. Each case is evaluated top-to-bottom until one matches.
switch { case score >= 90: grade = "A" case score >= 80: grade = "B" case score >= 70: grade = "C" default: grade = "F" } // Time-based logic switch hour := time.Now().Hour(); { case hour < 12: fmt.Println("Morning") case hour < 17: fmt.Println("Afternoon") default: fmt.Println("Evening") }
Fallthrough
Unlike C, Go's switch doesn't fall through by default. Use fallthrough keyword explicitly when needed—it's rare and transfers control unconditionally to the next case.
switch n { case 1: fmt.Println("one") fallthrough // Continues to next case case 2: fmt.Println("two") // Prints for both n=1 and n=2 case 3: fmt.Println("three") // Only prints for n=3 } // Output for n=1: "one" then "two"
Type switch
Type switches determine the dynamic type of an interface value. Use .(type) syntax only in switch statements—great for handling multiple types from an interface.
func describe(i interface{}) { switch v := i.(type) { case int: fmt.Printf("int: %d\n", v) case string: fmt.Printf("string: %s\n", v) case bool: fmt.Printf("bool: %t\n", v) case []int: fmt.Printf("[]int with len %d\n", len(v)) default: fmt.Printf("unknown type: %T\n", v) } }
defer statement
defer schedules a function call to run after the surrounding function returns—perfect for cleanup like closing files or unlocking mutexes. Arguments are evaluated immediately but the call executes later.
func readFile(name string) error { f, err := os.Open(name) if err != nil { return err } defer f.Close() // Guaranteed to run on return // Read file... even if panic occurs, defer runs return nil }
Multiple defers (LIFO)
Multiple defers execute in Last-In-First-Out order—like a stack. This naturally handles paired operations like acquiring/releasing locks in the correct order.
func example() { defer fmt.Println("first") defer fmt.Println("second") defer fmt.Println("third") fmt.Println("main") } // Output: main, third, second, first func lockBoth() { mu1.Lock() defer mu1.Unlock() mu2.Lock() defer mu2.Unlock() // Unlocks: mu2 first, then mu1 (correct order!) }
defer in loops
Defers in loops accumulate until the function returns—potentially causing resource leaks or memory buildup. Extract the loop body to a separate function to defer per-iteration.
// BAD: All files stay open until function returns func processFiles(names []string) { for _, name := range names { f, _ := os.Open(name) defer f.Close() // Accumulates! } } // GOOD: Each file closes after processing func processFiles(names []string) { for _, name := range names { processFile(name) // defer inside here } } func processFile(name string) { f, _ := os.Open(name) defer f.Close() // Runs at function end // process... }
Functions
Function declaration
Functions are declared with func, name, parameters, return types, and body. Go is pass-by-value: parameters are copies (except slices, maps, channels which contain pointers).
func functionName(param1 type1, param2 type2) returnType { return value } // Same-type parameters can share type func add(a, b int) int { return a + b } // No return value func greet(name string) { fmt.Println("Hello,", name) }
Function parameters
Parameters are passed by value (copied). For large structs, use pointers to avoid copying. Slices, maps, and channels contain internal pointers so modifications affect the original.
func modify(x int, s []int, m map[string]int) { x = 100 // Local copy changed s[0] = 100 // Original slice modified! m["a"] = 100 // Original map modified! } func main() { n := 1 sl := []int{1, 2, 3} mp := map[string]int{"a": 1} modify(n, sl, mp) // n == 1, sl == [100,2,3], mp == {"a":100} }
Multiple return values
Functions can return multiple values—idiomatic for returning results with errors. Callers must handle all values or use _ to discard.
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) } // Discard error (usually bad practice) result, _ = divide(10, 2)
Named return values
Return values can be named, acting as variables initialized to zero values. A naked return returns current values—use sparingly as it can reduce readability.
func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return // Returns x and y } // Useful for documenting what's returned func getUser(id int) (user *User, found bool) { // ... }
Variadic functions
Variadic functions accept any number of arguments of a type, accessed as a slice. Use ... in declaration; pass slice with slice... syntax.
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 // Pass slice as variadic args numbers := []int{1, 2, 3} sum(numbers...) // 6 // Common example: fmt.Printf fmt.Printf("%s is %d years old\n", name, age)
Functions as values
Functions are first-class values—they can be assigned to variables, passed as arguments, and returned from functions. The function type includes parameter and return types.
// Function type type operation func(int, int) int func main() { // Assign function to variable var op operation = add fmt.Println(op(2, 3)) // 5 op = multiply fmt.Println(op(2, 3)) // 6 } func add(a, b int) int { return a + b } func multiply(a, b int) int { return a * b }
Function closures
Closures are functions that capture and reference variables from their enclosing scope. The captured variables remain live as long as the closure exists—powerful for state and callbacks.
func counter() func() int { count := 0 return func() int { count++ // Captures 'count' from outer scope return count } } func main() { c := counter() fmt.Println(c()) // 1 fmt.Println(c()) // 2 fmt.Println(c()) // 3 c2 := counter() // New counter, own state fmt.Println(c2()) // 1 }
Recursive functions
Functions can call themselves. Go doesn't optimize tail recursion, so deep recursion can stack overflow—prefer iteration for deep recursion or use explicit stacks.
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) } // Tree traversal func traverse(node *Node) { if node == nil { return } traverse(node.Left) process(node) traverse(node.Right) }
Anonymous functions
Anonymous functions have no name and can be declared inline. They're often used immediately (IIFE), as goroutines, or passed as callbacks.
// Immediately invoked func() { fmt.Println("Anonymous!") }() // Assigned to variable double := func(x int) int { return x * 2 } // As goroutine go func(msg string) { fmt.Println(msg) }("hello") // As callback sort.Slice(people, func(i, j int) bool { return people[i].Age < people[j].Age })
Higher-order functions
Higher-order functions take functions as parameters or return functions—enabling functional programming patterns like map, filter, reduce (though Go doesn't have these built-in).
// Takes function as parameter func apply(nums []int, fn func(int) int) []int { result := make([]int, len(nums)) for i, n := range nums { result[i] = fn(n) } return result } // Returns function func multiplier(factor int) func(int) int { return func(x int) int { return x * factor } } double := multiplier(2) apply([]int{1, 2, 3}, double) // [2, 4, 6]
init function
init functions run automatically after package variables are initialized, before main. A package can have multiple init functions (even in same file)—used for setup, validation, registration.
package main var config Config func init() { // Runs before main config = loadConfig() log.Println("Config loaded") } func init() { // Multiple inits allowed, run in order validateConfig(config) } func main() { // Both inits have run }
┌─────────────────────────────────────────────┐ │ Initialization Order: │ │ 1. Imported packages (recursively) │ │ 2. Package-level variables │ │ 3. init() functions (in source order) │ │ 4. main() function │ └─────────────────────────────────────────────┘
main function
main is the entry point for executable programs—must be in package main with no parameters and no return value. Use os.Exit(code) for exit codes.
package main import "os" func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run() error { // Actual program logic // This pattern allows defers to run before exit return nil }
Pointers
Pointer basics
Pointers hold memory addresses of values. Go pointers are type-safe (no pointer arithmetic) and garbage-collected. They enable sharing and mutation of values across function boundaries.
┌─────────────────────────────────────────────┐ │ Variable x Pointer p │ │ ┌─────────┐ ┌─────────┐ │ │ │ 42 │ ◄──── │ &x │ │ │ └─────────┘ └─────────┘ │ │ addr: 0x1000 value: 0x1000 │ │ │ │ x = 42 p = &x │ │ *p = 42 │ └─────────────────────────────────────────────┘
& (address-of operator)
The & operator returns the memory address of a variable, creating a pointer. You can only take the address of addressable values (variables, struct fields, array/slice elements).
x := 42 p := &x // p is *int, holds address of x fmt.Println(p) // 0xc0000b4008 (memory address) fmt.Println(*p) // 42 (value at address) // Can't take address of literals or constants // p := &42 // Error! // Can take address of composite literals user := &User{Name: "Alice"} // Creates and returns pointer
* (dereference operator)
The * operator has two meanings: in type declarations it denotes a pointer type (*int), and as an operator it dereferences a pointer to access/modify the underlying value.
var p *int // p is a pointer to int (nil by default) x := 42 p = &x // p points to x fmt.Println(*p) // 42 (read through pointer) *p = 100 // Modify through pointer fmt.Println(x) // 100 (x changed!) // Nil pointer dereference panics var np *int // fmt.Println(*np) // PANIC: nil pointer dereference
Pointer receivers
Methods with pointer receivers can modify the receiver and avoid copying—use for large structs or when modification is needed. Go auto-dereferences and auto-references for method calls.
type Counter struct { value int } // Value receiver: works on copy func (c Counter) Get() int { return c.value } // Pointer receiver: modifies original func (c *Counter) Increment() { c.value++ } func main() { c := Counter{} c.Increment() // Auto-referenced: (&c).Increment() fmt.Println(c.Get()) // 1 p := &Counter{} p.Increment() fmt.Println(p.Get()) // 1, auto-dereferenced }
nil pointers
Uninitialized pointers are nil. Dereferencing nil panics. Methods with pointer receivers can be called on nil if they handle it—a useful pattern for optional values.
var p *int // nil pointer fmt.Println(p) // <nil> fmt.Println(p == nil) // true // *p = 42 // PANIC! // Methods can handle nil receivers type Node struct { Value int Next *Node } func (n *Node) String() string { if n == nil { return "<nil>" } return fmt.Sprintf("%d", n.Value) } var node *Node fmt.Println(node.String()) // "<nil>" - works!
Pointers vs values
Use pointers when: modifying the receiver, struct is large (>64 bytes), you need nil semantics, or consistency (if one method uses pointer, all should). Use values for small immutable types.
┌────────────────────────────────────────────────────┐ │ USE POINTERS: │ USE VALUES: │ │ • Modify receiver │ • Small structs │ │ • Large structs │ • Immutable types │ │ • Need nil semantics │ • Basic types │ │ • Shared ownership │ • When copying is cheap │ │ • Consistency in API │ • Thread safety │ ├────────────────────────────────────────────────────┤ │ Note: Slices, maps, channels already contain │ │ pointers - don't use **slice or *map │ └────────────────────────────────────────────────────┘
When to use pointers
Pointers are essential for modification and efficiency, but overuse adds complexity. Default to values; use pointers deliberately.
// Pointer: Modify in function func increment(x *int) { *x++ } // Pointer: Large struct type LargeConfig struct { /* many fields */ } func process(c *LargeConfig) { } // Pointer: Optional/nil func findUser(id int) *User { return nil } // Value: Small immutable type Point struct { X, Y int } func (p Point) Distance() float64 { ... } // Value: Return copies func (u User) Name() string { return u.name }
Data Structures
Arrays
Array declaration
Arrays have fixed size, part of the type—[3]int and [4]int are different types. Size must be a compile-time constant. Arrays are values, not references—assignment copies all elements.
var a [5]int // Zero value: [0,0,0,0,0] var b [3]string // ["","",""] var c [2][3]int // 2x3 matrix of zeros // Size is part of type var d [3]int var e [4]int // d = e // Error: different types!
Array literals
Initialize arrays with literal syntax. Use ... to infer size from elements. Index-based initialization allows sparse arrays.
a := [3]int{1, 2, 3} b := [...]int{1, 2, 3, 4, 5} // Size inferred: [5]int // Indexed initialization c := [5]int{0: 100, 4: 400} // [100, 0, 0, 0, 400] // Mixed d := [5]int{1, 2, 4: 99} // [1, 2, 0, 0, 99]
Array length
len() returns array length, known at compile time. Arrays can't be resized—use slices for dynamic sizing.
arr := [5]int{1, 2, 3, 4, 5} fmt.Println(len(arr)) // 5 // Length is compile-time constant const size = len(arr) // Valid // No capacity for arrays (cap == len always) fmt.Println(cap(arr)) // 5
Iterating arrays
Use for with index or range for iteration. Range provides index and value copy—modifying the value doesn't affect the array.
arr := [3]string{"a", "b", "c"} // Index-based for i := 0; i < len(arr); i++ { fmt.Println(arr[i]) } // Range (index and value) for i, v := range arr { fmt.Println(i, v) } // Value only for _, v := range arr { fmt.Println(v) } // Index only for i := range arr { arr[i] = "x" // Modify in place }
Multi-dimensional arrays
Multi-dimensional arrays are arrays of arrays. Each dimension has fixed size. Access with multiple indices.
// 3x3 matrix var matrix [3][3]int // Initialize matrix = [3][3]int{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, } // Access fmt.Println(matrix[1][2]) // 6 (row 1, col 2) // Iterate for i := range matrix { for j := range matrix[i] { fmt.Printf("%d ", matrix[i][j]) } fmt.Println() }
Array comparison
Arrays are comparable if their element type is comparable. Two arrays are equal if they have the same type and all elements are equal.
a := [3]int{1, 2, 3} b := [3]int{1, 2, 3} c := [3]int{1, 2, 4} fmt.Println(a == b) // true fmt.Println(a == c) // false // Can use arrays as map keys counts := make(map[[2]int]int) counts[[2]int{1, 2}] = 5 // Slices are NOT comparable // []int{1,2} == []int{1,2} // Error!
Slices
Slice basics
Slices are dynamic, flexible views into arrays—the workhorse of Go. A slice has three components: pointer to underlying array, length, and capacity. They're reference types—assignment shares the underlying array.
┌─────────────────────────────────────────────────┐ │ Slice Header (24 bytes on 64-bit): │ │ ┌─────────┬─────────┬─────────┐ │ │ │ ptr │ len │ cap │ │ │ └────┬────┴─────────┴─────────┘ │ │ │ │ │ ▼ │ │ ┌───┬───┬───┬───┬───┬───┬───┐ │ │ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ (backing array)│ │ └───┴───┴───┴───┴───┴───┴───┘ │ │ ↑ ↑ ↑ │ │ ptr len=4 cap=7 │ └─────────────────────────────────────────────────┘
Slice literals
Slice literals look like array literals without the size. They create an underlying array and return a slice referencing it.
s := []int{1, 2, 3, 4, 5} // Creates array, returns slice fmt.Println(len(s), cap(s)) // 5, 5 // Empty slice (not nil) empty := []int{} fmt.Println(len(empty) == 0) // true fmt.Println(empty == nil) // false // Indexed initialization s2 := []int{0: 10, 4: 50} // [10, 0, 0, 0, 50]
Slice length and capacity
Length is current elements; capacity is maximum before reallocation (from start to end of backing array). Capacity matters for performance—growing beyond it requires allocation and copy.
s := make([]int, 3, 5) fmt.Println(len(s), cap(s)) // 3, 5 // Length: elements accessible // Capacity: can grow without reallocation arr := [5]int{1, 2, 3, 4, 5} slice := arr[1:3] // [2, 3] fmt.Println(len(slice)) // 2 fmt.Println(cap(slice)) // 4 (from index 1 to end)
make function for slices
make creates slices with specified length and optional capacity. Use when you know the size upfront—avoids reallocations during append.
// make([]T, length, capacity) s1 := make([]int, 5) // len=5, cap=5, all zeros s2 := make([]int, 0, 100) // len=0, cap=100, pre-allocated // Pre-allocate for known size func collect(n int) []int { result := make([]int, 0, n) for i := 0; i < n; i++ { result = append(result, i) // No reallocation } return result }
Slice operations (append, copy)
append adds elements, growing slice as needed (returns new slice!). copy copies elements between slices, returning count copied. Both are safe with nil slices.
// Append (always reassign!) s := []int{1, 2, 3} s = append(s, 4) // [1, 2, 3, 4] s = append(s, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7] s = append(s, []int{8, 9}...) // Append slice // Copy src := []int{1, 2, 3} dst := make([]int, len(src)) n := copy(dst, src) // n=3, dst=[1,2,3] // Copy handles size mismatch small := make([]int, 2) copy(small, src) // copies only 2 elements
Slice expressions (slicing)
Slice expressions create new slices from arrays, slices, or strings. Format: a[low:high] or a[low:high:max]. Result shares underlying array.
arr := [5]int{0, 1, 2, 3, 4} s := arr[1:4] // [1, 2, 3] len=3, cap=4 s = arr[:3] // [0, 1, 2] low defaults to 0 s = arr[2:] // [2, 3, 4] high defaults to len s = arr[:] // [0, 1, 2, 3, 4] full slice // Three-index slice: a[low:high:max] s = arr[1:3:4] // [1, 2] len=2, cap=3 (limited!) // Useful to prevent append affecting original s = arr[1:3:3] // cap=2, append will allocate new array
Re-slicing
Slices can be resliced within capacity bounds—extending length up to capacity. Cannot extend beyond capacity or below zero.
s := make([]int, 3, 5) // [0, 0, 0], cap=5 s = s[:5] // [0, 0, 0, 0, 0] extend to cap s = s[2:] // [0, 0, 0], cap now 3 // Can't exceed capacity // s = s[:10] // panic: out of range // Re-slice to "recover" capacity a := []int{1, 2, 3, 4, 5} b := a[2:4] // [3, 4], cap=3 c := b[:cap(b)] // [3, 4, 5] - "recovered" element
nil slices vs empty slices
A nil slice has nil pointer, zero length and capacity. An empty slice has a pointer (non-nil) but zero length. Both behave identically with len, append, range.
var nilSlice []int // nil emptySlice := []int{} // empty, not nil makeSlice := make([]int, 0) // empty, not nil fmt.Println(nilSlice == nil) // true fmt.Println(emptySlice == nil) // false // All work the same fmt.Println(len(nilSlice)) // 0 nilSlice = append(nilSlice, 1) // Works! for range nilSlice { } // Works (zero iterations) // JSON difference! json.Marshal(nilSlice) // null json.Marshal(emptySlice) // []
Slice internals
Understanding slice internals prevents bugs. Slices share underlying arrays until append forces reallocation. Always be aware of aliasing when passing slices.
original := []int{1, 2, 3, 4, 5} slice1 := original[1:4] // [2, 3, 4] shares array slice2 := original[2:5] // [3, 4, 5] shares same array slice1[1] = 99 // Modifies original[2]! fmt.Println(original) // [1, 2, 99, 4, 5] fmt.Println(slice2) // [99, 4, 5] - affected! // After append beyond capacity, new array slice1 = append(slice1, 100, 200, 300) slice1[0] = 0 // Doesn't affect original anymore
Slice pitfalls
Common slice mistakes: not reassigning append result, memory leaks from small slices of large arrays, and unintended sharing.
// Pitfall 1: Ignoring append return s := []int{1, 2, 3} append(s, 4) // WRONG: result discarded s = append(s, 4) // Correct // Pitfall 2: Memory leak from large backing array func getFirstThree(large []byte) []byte { return large[:3] // Holds reference to entire large array! } func getFirstThreeSafe(large []byte) []byte { result := make([]byte, 3) copy(result, large[:3]) return result // New small allocation } // Pitfall 3: Loop variable capture (fixed in Go 1.22) for _, v := range values { go func() { fmt.Println(v) // Pre-1.22: prints last value only }() }
Multi-dimensional slices
Unlike arrays, multi-dimensional slices have independent dimensions. Each row can have different lengths (jagged array). Allocate carefully for memory efficiency.
// Create 3x4 matrix matrix := make([][]int, 3) for i := range matrix { matrix[i] = make([]int, 4) } // Or use single allocation (more efficient) rows, cols := 3, 4 flat := make([]int, rows*cols) matrix2 := make([][]int, rows) for i := range matrix2 { matrix2[i] = flat[i*cols : (i+1)*cols] } // Jagged array (different row lengths) jagged := [][]int{ {1, 2, 3}, {4, 5}, {6, 7, 8, 9}, }
Maps
Map basics
Maps are hash tables mapping keys to values. They're reference types—passing a map shares it. Zero value is nil (must initialize before writing). Keys must be comparable.
// Declaration (nil map - can read, can't write) var m map[string]int // Initialization with make m = make(map[string]int) m["age"] = 30 // Map literal scores := map[string]int{ "Alice": 95, "Bob": 87, "Carol": 92, }
Map literals
Map literals provide inline initialization. Keys must be unique. Empty map literal creates non-nil empty map.
// Simple literal m := map[string]int{"a": 1, "b": 2} // Complex value types users := map[int]User{ 1: {Name: "Alice", Age: 30}, 2: {Name: "Bob", Age: 25}, } // Map of slices graph := map[string][]string{ "A": {"B", "C"}, "B": {"A", "D"}, } // Empty map (not nil) empty := map[string]int{}
make function for maps
make creates initialized maps, optionally with capacity hint. Capacity hint improves performance when size is known—avoids rehashing during growth.
// Basic m := make(map[string]int) // With capacity hint (not a limit!) m = make(map[string]int, 1000) // Capacity hint for known sizes func wordCount(text string) map[string]int { words := strings.Fields(text) counts := make(map[string]int, len(words)) for _, w := range words { counts[w]++ } return counts }
Map operations (insert, update, delete, lookup)
Maps support CRUD operations with simple syntax. Lookup returns zero value for missing keys; use comma-ok idiom to check existence.
m := make(map[string]int) // Insert / Update m["a"] = 1 m["a"] = 2 // Update // Lookup v := m["a"] // 2 v = m["missing"] // 0 (zero value) // Check existence (comma-ok idiom) v, ok := m["a"] if !ok { fmt.Println("key not found") } // Delete delete(m, "a") // No-op if key doesn't exist // Increment idiom m["counter"]++ // Works even if key doesn't exist
Map iteration
Range over maps yields key-value pairs in random order—deliberately not guaranteed. To iterate in order, sort keys first.
m := map[string]int{"c": 3, "a": 1, "b": 2} // Random order each time! for k, v := range m { fmt.Println(k, v) } // Keys only for k := range m { fmt.Println(k) } // Sorted iteration keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Println(k, m[k]) }
nil maps
A nil map is a read-only empty map. Reading returns zero values, but writing panics. Always initialize maps before writing.
var m map[string]int // nil map // Reading nil map is safe v := m["key"] // 0 (zero value) _, ok := m["key"] // ok = false // Writing nil map panics! // m["key"] = 1 // panic: assignment to nil map // Length is 0 fmt.Println(len(m)) // 0 // Range is safe (zero iterations) for k, v := range m { } // Check for nil before writing if m == nil { m = make(map[string]int) } m["key"] = 1
Map key requirements
Map keys must be comparable with == (not slices, maps, or functions). Common key types: strings, integers, pointers, structs with comparable fields, arrays.
// Valid key types m1 := map[string]int{} // strings m2 := map[int]string{} // integers m3 := map[*User]bool{} // pointers m4 := map[[2]int]bool{} // arrays // Struct keys (all fields must be comparable) type Point struct { X, Y int } m5 := map[Point]string{ {1, 2}: "A", {3, 4}: "B", } // Invalid key types // map[[]int]bool{} // slices: not comparable // map[map[int]int]bool{} // maps: not comparable // map[func()]bool{} // functions: not comparable
Map internals
Maps use hash tables with buckets. Operations are O(1) average. Maps are not safe for concurrent access—use sync.Mutex or sync.Map.
┌─────────────────────────────────────────────────┐ │ Map internals (simplified): │ │ │ │ ┌────────────────────────────────────────┐ │ │ │ hash("key") → bucket index │ │ │ └────────────────────────────────────────┘ │ │ ↓ │ │ Bucket 0: [key1:val1] → [key2:val2] → ... │ │ Bucket 1: [key3:val3] │ │ Bucket 2: empty │ │ ... │ └─────────────────────────────────────────────────┘
// NOT safe for concurrent access // Use mutex: var mu sync.Mutex mu.Lock() m["key"] = value mu.Unlock()
sync.Map
sync.Map is a concurrent-safe map optimized for two patterns: write-once-read-many, or disjoint key access. For other patterns, regular map with mutex is often faster.
var m sync.Map // Store m.Store("key", "value") // Load v, ok := m.Load("key") if ok { fmt.Println(v.(string)) // Type assertion needed } // LoadOrStore (atomic get-or-set) actual, loaded := m.LoadOrStore("key", "default") // Delete m.Delete("key") // Range (not snapshot, may see concurrent changes) m.Range(func(k, v interface{}) bool { fmt.Println(k, v) return true // Continue iteration })
Structs
Struct declaration
Structs are typed collections of fields—Go's main way to create custom data types. Fields can be any type, including other structs, pointers, slices, maps.
type Person struct { Name string Age int Email string Address *Address // Pointer to another struct } type Address struct { Street string City string Country string } // Empty struct (zero size, useful for sets) type void struct{} set := map[string]void{}
Struct literals
Create struct values with literals. Field names can be omitted if providing all fields in order (not recommended for readability and maintenance).
// Named fields (preferred) p1 := Person{ Name: "Alice", Age: 30, Email: "alice@example.com", } // Positional (fragile, avoid) p2 := Person{"Bob", 25, "bob@example.com", nil} // Partial initialization (others get zero values) p3 := Person{Name: "Carol"} // Age=0, Email="" // Pointer to struct p4 := &Person{Name: "Dave"}
Accessing struct fields
Use dot notation for field access. Works the same for struct values and pointers—Go auto-dereferences pointers.
p := Person{Name: "Alice", Age: 30} // Read fmt.Println(p.Name) // Alice // Write p.Age = 31 // Pointer access (auto-dereferenced) pp := &p fmt.Println(pp.Name) // Alice (same as (*pp).Name) pp.Age = 32 // Nested structs p.Address = &Address{City: "NYC"} fmt.Println(p.Address.City) // NYC
Struct pointers
Use pointers to avoid copying large structs and to allow modification. The &StructType{} syntax creates a struct and returns its address.
// Pointer to struct literal p := &Person{Name: "Alice"} // Equivalent to: var p2 Person p2 = Person{Name: "Alice"} ptr := &p2 // new() returns pointer to zero-value struct p3 := new(Person) // *Person, all fields zero // Modifying through pointer func birthday(p *Person) { p.Age++ } birthday(p) // p.Age is modified
Anonymous structs
Anonymous structs have no named type—useful for one-off data structures, test fixtures, or quick grouping. Common in table-driven tests.
// Inline declaration and initialization point := struct { X, Y int }{10, 20} // Table-driven tests tests := []struct { name string input int expected int }{ {"positive", 5, 25}, {"negative", -3, 9}, {"zero", 0, 0}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // test logic }) }
Embedded structs (composition)
Go uses composition over inheritance. Embedded structs promote their fields and methods—you can access them directly on the outer struct.
type Person struct { Name string Age int } type Employee struct { Person // Embedded (no field name) EmployeeID string Department string } func main() { e := Employee{ Person: Person{Name: "Alice", Age: 30}, EmployeeID: "E123", } // Fields promoted fmt.Println(e.Name) // Shorthand for e.Person.Name fmt.Println(e.Age) // Methods also promoted e.Greet() // If Person has Greet() method }
Struct tags
Tags are string metadata attached to fields—used by encoding/json, databases, validation, etc. Access via reflection. Follow key:"value" convention.
type User struct { ID int `json:"id" db:"user_id"` Name string `json:"name" validate:"required"` Email string `json:"email,omitempty"` Password string `json:"-"` // Excluded from JSON CreatedAt time.Time `json:"created_at" db:"created_at"` } // Common tags: // json:"name" - JSON field name // json:",omitempty" - Omit if zero value // json:"-" - Ignore field // db:"column" - Database column // xml:"element" - XML element name // validate:"required" - Validation rule
Struct comparison
Structs are comparable if all fields are comparable. Two structs are equal if all fields are equal. Uncomparable fields (slices, maps, functions) make the struct uncomparable.
type Point struct { X, Y int } p1 := Point{1, 2} p2 := Point{1, 2} p3 := Point{1, 3} fmt.Println(p1 == p2) // true fmt.Println(p1 == p3) // false // Can be map keys locations := map[Point]string{ {0, 0}: "origin", } // Uncomparable structs type Data struct { Values []int // Slices not comparable } // d1 == d2 // Compile error! // Use reflect.DeepEqual for uncomparable types reflect.DeepEqual(d1, d2)
Exported vs unexported fields
Capital letter fields are exported (public), lowercase are unexported (private to package). Unexported fields are invisible to JSON, other packages, and reflection from outside.
package user type User struct { ID int // Exported Name string // Exported password string // unexported - private } // From another package: u := user.User{ ID: 1, Name: "Alice", // password: "secret", // Error: unknown field } fmt.Println(u.Name) // OK // fmt.Println(u.password) // Error: unexported field // JSON ignores unexported fields json.Marshal(u) // {"ID":1,"Name":"Alice"}
Strings
String basics
Strings in Go are immutable sequences of bytes (not characters!), typically holding UTF-8 encoded text. They're value types but efficient because they're backed by a pointer and length.
┌─────────────────────────────────────────────────┐ │ String header (16 bytes): │ │ ┌────────────────┬────────────────┐ │ │ │ ptr (*byte) │ len (int) │ │ │ └───────┬────────┴────────────────┘ │ │ │ │ │ ▼ │ │ ┌───┬───┬───┬───┬───┐ │ │ │ H │ e │ l │ l │ o │ (immutable bytes) │ │ └───┴───┴───┴───┴───┘ │ └─────────────────────────────────────────────────┘
s := "Hello, 世界" fmt.Println(len(s)) // 13 (bytes, not characters!)
String literals (interpreted vs raw)
Interpreted strings use double quotes with escape sequences. Raw strings use backticks—no escapes, can span multiple lines. Use raw for regex, paths, multi-line content.
// Interpreted strings (double quotes) s1 := "Hello\nWorld" // Contains newline s2 := "Tab\there" // Contains tab s3 := "Quote: \"Hi\"" // Escaped quote s4 := "Path: C:\\Users" // Escaped backslash // Raw strings (backticks) s5 := `Hello World` // Literal newline s6 := `C:\Users\file.txt` // No escaping needed s7 := `Line1 Line2 Line3` // Multi-line regex := `\d{3}-\d{4}` // Regex without escaping
String immutability
Strings cannot be modified after creation—this enables safe sharing and simple equality. To "modify" a string, create a new one.
s := "hello" // s[0] = 'H' // Compile error! // Create new string instead s = "H" + s[1:] // "Hello" s = strings.ToUpper(s) // "HELLO" // For heavy modification, use []byte or strings.Builder b := []byte(s) b[0] = 'h' s = string(b) // "hELLO"
String concatenation
Use + for simple concatenation. For many strings, use strings.Builder for O(n) performance instead of O(n²) with repeated +.
// Simple (creates new string each time) s := "Hello" + ", " + "World" // In loop - inefficient O(n²) result := "" for i := 0; i < 1000; i++ { result += "x" // Creates new string each time! } // Use strings.Builder - efficient O(n) var builder strings.Builder for i := 0; i < 1000; i++ { builder.WriteString("x") } result = builder.String() // Or strings.Join for slices parts := []string{"a", "b", "c"} s = strings.Join(parts, ",") // "a,b,c"
String length (len vs utf8.RuneCountInString)
len() returns bytes count, not character count. For Unicode character (rune) count, use utf8.RuneCountInString. This distinction is crucial for internationalization.
s := "Hello, 世界" fmt.Println(len(s)) // 13 bytes fmt.Println(utf8.RuneCountInString(s)) // 9 characters // Why? ASCII: 1 byte each, Chinese: 3 bytes each // H e l l o , 世 界 // 1 1 1 1 1 1 1 3 3 = 13 bytes // 1 1 1 1 1 1 1 1 1 = 9 runes // Common mistake emoji := "👋" fmt.Println(len(emoji)) // 4 bytes fmt.Println(utf8.RuneCountInString(emoji)) // 1 rune
Runes and UTF-8
A rune is an alias for int32, representing a Unicode code point. Go source code is UTF-8, and strings store UTF-8 bytes. Convert between strings, runes, and bytes carefully.
r := '世' // rune literal (int32) fmt.Printf("%T: %d %c\n", r, r, r) // int32: 19990 世 s := "世界" runes := []rune(s) // Convert to runes fmt.Println(len(runes)) // 2 runes fmt.Println(runes[0]) // 19990 // Back to string s2 := string(runes) s3 := string(r) // Single rune to string // Rune from integer fmt.Println(string(65)) // "A" (ASCII 65) fmt.Println(string(0x4e16)) // "世" (Unicode)
Byte slices vs strings
Strings and []byte are interconvertible but different: strings are immutable and comparable, byte slices are mutable. Conversion copies data (usually).
s := "Hello" b := []byte(s) // Copy: string → []byte s2 := string(b) // Copy: []byte → string // Byte slice is mutable b[0] = 'h' fmt.Println(string(b)) // "hello" fmt.Println(s) // "Hello" (unchanged) // Avoid copies in hot paths - use unsafe if needed // Many standard library functions accept both: strings.Contains(s, "ell") // string bytes.Contains(b, []byte("ell")) // []byte
String iteration
Range over strings yields rune index and rune value—handling multi-byte characters correctly. Index-based access gives bytes, not runes.
s := "Go日本" // By byte (often wrong for Unicode) for i := 0; i < len(s); i++ { fmt.Printf("%d: %x\n", i, s[i]) // Bytes, not chars } // By rune (correct for Unicode) for i, r := range s { fmt.Printf("%d: %c\n", i, r) } // Output: // 0: G // 1: o // 2: 日 (index 2, even though 3 bytes) // 5: 本 (index 5) // To index by rune position runes := []rune(s) fmt.Println(string(runes[2])) // 日
strings package
The strings package provides essential string manipulation functions—searching, replacing, splitting, joining, case conversion, and trimming.
import "strings" s := "Hello, World!" // Searching strings.Contains(s, "World") // true strings.HasPrefix(s, "Hello") // true strings.HasSuffix(s, "!") // true strings.Index(s, "o") // 4 strings.Count(s, "l") // 3 // Transforming strings.ToUpper(s) // "HELLO, WORLD!" strings.ToLower(s) // "hello, world!" strings.TrimSpace(" hi ") // "hi" strings.Trim(s, "!") // "Hello, World" strings.Replace(s, "l", "L", 2) // "HeLLo, World!" strings.ReplaceAll(s, "l", "L") // "HeLLo, WorLd!" // Splitting/Joining strings.Split("a,b,c", ",") // ["a", "b", "c"] strings.Fields("a b c") // ["a", "b", "c"] strings.Join([]string{"a","b"}, "-") // "a-b"
strconv package
strconv handles conversions between strings and basic types—parsing strings to numbers and formatting numbers to strings.
import "strconv" // String → Int i, err := strconv.Atoi("42") // 42 i64, err := strconv.ParseInt("42", 10, 64) // int64 // Int → String s := strconv.Itoa(42) // "42" s = strconv.FormatInt(42, 16) // "2a" (hex) // String → Float f, err := strconv.ParseFloat("3.14", 64) // float64 // Float → String s = strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14" // String → Bool b, err := strconv.ParseBool("true") // true // Bool → String s = strconv.FormatBool(true) // "true" // Quote/Unquote s = strconv.Quote("hello\n") // `"hello\n"` s, err = strconv.Unquote(`"hello"`) // "hello"
String builders (strings.Builder)
strings.Builder efficiently builds strings by minimizing allocations. It's the idiomatic way to construct strings incrementally—much faster than += in loops.
var b strings.Builder // Write methods b.WriteString("Hello") b.WriteString(", ") b.WriteString("World!") b.WriteByte('!') b.WriteRune('🎉') result := b.String() // "Hello, World!!🎉" // Pre-allocate for known size b.Grow(1000) // Reset for reuse b.Reset() // Common pattern: building with format var sb strings.Builder for i, name := range names { if i > 0 { sb.WriteString(", ") } sb.WriteString(name) } list := sb.String() // "Alice, Bob, Carol"
Unicode handling
Go handles Unicode through the unicode and unicode/utf8 packages. Always be Unicode-aware when processing international text.
import ( "unicode" "unicode/utf8" ) s := "Hello, 世界! 🎉" // Character classification unicode.IsLetter('A') // true unicode.IsDigit('9') // true unicode.IsSpace(' ') // true unicode.IsPunct('!') // true unicode.IsUpper('A') // true unicode.ToUpper('a') // 'A' // UTF-8 validation utf8.ValidString(s) // true utf8.Valid([]byte(s)) // true // Rune handling utf8.RuneCountInString(s) // 12 characters utf8.RuneLen('世') // 3 bytes needed // Decode first rune r, size := utf8.DecodeRuneInString(s) fmt.Printf("%c uses %d bytes\n", r, size) // Check for invalid UTF-8 if r == utf8.RuneError { // Invalid UTF-8 sequence }