Go in 2 Weeks #02 | Day 3–4: Memory & Data Structures — Pointers Without Pointer Arithmetic

Go in 2 Weeks #02 | Day 3–4: Memory & Data Structures — Pointers Without Pointer Arithmetic

이 글의 핵심

Compare Go pointers, slices, and maps to C++ pointers, vector, and unordered_map. Practical patterns for len, cap, append, and the comma-ok idiom.

Series overview

📚 Go in 2 Weeks #02 | Full series index

This post covers Days 3–4 of the two-week Go curriculum for C++ developers.

Previous: #01 Philosophy & syntax ← | → Next: #03 OOP & composition


Introduction: the world of safe pointers

In C++, pointer arithmetic (p++, p + offset) lets you walk memory freely—but segmentation faults come with the territory. Go has pointers but no pointer arithmetic. Safety was chosen on purpose. This article compares Go pointers and core data structures—slices and maps—to C++.

You will learn:

  • Go pointer restrictions and safety
  • Call by value vs pointers
  • Slice length and capacity
  • Map usage and pitfalls

Real-world notes

Lessons from adopting Go in real projects.

Moving from C++ to Go

I spent over a decade building servers in C++. Go felt almost too simple at first. In production, that simplicity became a strength.

Takeaways:

  • Faster delivery: work that took three days in C++ often took one in Go
  • Stability: fewer memory-leak worries with the GC
  • Deployment: single static binaries simplify rollout

This series reflects that experience.


Table of contents

  1. Pointers: no arithmetic, but dereference works
  2. Arrays: fixed-size value types
  3. Slices: Go’s dynamic arrays
  4. Maps: hash tables
  5. Exercises

1. Pointers: no arithmetic, but dereference works

C++ vs Go: pointer basics

// C++: pointer arithmetic allowed
int x = 10;
int* p = &x;

*p = 20;        // dereference
p++;            // pointer arithmetic (next int)
*(p + 5) = 30;  // offset access

int arr[10];
int* ptr = arr;
ptr[5] = 100;   // array indexing = pointer arithmetic
// Go: no pointer arithmetic
x := 10
p := &x

*p = 20         // ✅ dereference OK
// p++          // ❌ compile error: no pointer arithmetic
// *(p + 5)     // ❌ compile error

// Use indexing for arrays
arr := [10]int{}
arr[5] = 100    // ✅ index access

Key differences:

  • Go does not allow pointer arithmetic (safety)
  • Only * (dereference) and & (address-of)
  • Array access uses index syntax only

C++ vs Go: function arguments

// C++: value, pointer, reference
void byValue(int x) {
    x = 100;  // original unchanged
}

void byPointer(int* p) {
    *p = 100;  // original changed
}

void byReference(int& r) {
    r = 100;  // original changed
}

int main() {
    int x = 10;
    byValue(x);        // x still 10
    byPointer(&x);     // x becomes 100
    byReference(x);    // x becomes 100
}
// Go: value or pointer (no references)
func byValue(x int) {
    x = 100  // original unchanged
}

func byPointer(p *int) {
    *p = 100  // original changed
}

func main() {
    x := 10
    byValue(x)      // x still 10
    byPointer(&x)   // x becomes 100
}

Pointer usage:

  • Small types (int, bool): pass by value
  • Large structs (~64+ bytes): pass pointer
  • Mutation needed: pass pointer
  • Read-only: value or pointer (stay consistent)

Nil pointers

// C++: nullptr
int* p = nullptr;
if (p == nullptr) {
    std::cout << "null pointer\n";
}

// Dereference → segfault
// *p = 10;  // crash!
// Go: nil
var p *int
if p == nil {
    fmt.Println("nil pointer")
}

// Dereference → panic
// *p = 10  // panic: runtime error: invalid memory address

2. Arrays: fixed-size value types

C++ vs Go: array declarations

// C++: array
int arr[5] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);  // 5

// Decays to pointer when passed to functions
void process(int* arr, int size) {
    // ...
}
// Go: array (size is part of the type)
var arr [5]int = [5]int{1, 2, 3, 4, 5}
// or
arr := [5]int{1, 2, 3, 4, 5}
// or (size inferred)
arr := [...]int{1, 2, 3, 4, 5}

length := len(arr)  // 5

// Passed by value—full copy (no decay to pointer)
func process(arr [5]int) {
    // entire array copied
}

Key differences:

  • In Go, array size is part of the type: [5]int and [10]int differ
  • Passing an array copies the whole array (no C-style decay)
  • In practice, slices are used far more often than arrays

3. Slices: Go’s dynamic arrays

Slices are the most common container in Go. They resemble std::vector but with different ergonomics.

C++ vs Go: dynamic arrays

// C++: std::vector
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec;
    
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);
    
    std::cout << "Size: " << vec.size() << "\n";
    std::cout << "Capacity: " << vec.capacity() << "\n";
    
    vec.reserve(100);
    
    for (const auto& v : vec) {
        std::cout << v << " ";
    }
}
// Go: slice
package main

import "fmt"

func main() {
    var slice []int  // nil slice
    
    slice = append(slice, 1)
    slice = append(slice, 2)
    slice = append(slice, 3)
    
    fmt.Println("Length:", len(slice))
    fmt.Println("Capacity:", cap(slice))
    
    slice2 := make([]int, 0, 100)  // len=0, cap=100
    
    for i, v := range slice {
        fmt.Println(i, v)
    }
}

Slice internals

// A slice is a small struct of three fields
type slice struct {
    ptr *[...]T  // pointer to backing array
    len int      // length
    cap int      // capacity
}
graph LR
    A[Slice] --> B["ptr: array pointer"]
    A --> C["len: length"]
    A --> D["cap: capacity"]
    B --> E[Backing array memory]

Creating slices

package main

import "fmt"

func main() {
    // 1. nil slice
    var s1 []int
    fmt.Println(s1 == nil)  // true
    
    // 2. literal
    s2 := []int{1, 2, 3}
    
    // 3. make (length and capacity)
    s3 := make([]int, 5)      // len=5, cap=5, zero-filled
    s4 := make([]int, 5, 10)  // len=5, cap=10
    
    // 4. slicing
    arr := [5]int{1, 2, 3, 4, 5}
    s5 := arr[1:4]  // [2, 3, 4], shares arr
    
    fmt.Println(s1, s2, s3, s4, s5)
}

Slice expressions

// C++: sub-range (copy)
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int> sub(vec.begin() + 1, vec.begin() + 4);  // [2, 3, 4]
// Go: slicing (shares backing array)
slice := []int{1, 2, 3, 4, 5}

sub := slice[1:4]   // [2, 3, 4], indices 1..3
sub[0] = 100        // slice[1] becomes 100 too!

// slice[low:high]   // low through high-1
// slice[low:]       // low to end
// slice[:high]      // start through high-1
// slice[:]          // whole slice (still a view, not a deep copy)

copied := make([]int, len(sub))
copy(copied, sub)

Caveat: slicing shares the backing array; writes through one slice affect the other.

append and reallocation

package main

import "fmt"

func main() {
    slice := make([]int, 0, 3)  // len=0, cap=3
    fmt.Printf("len=%d cap=%d\n", len(slice), cap(slice))
    
    slice = append(slice, 1)
    slice = append(slice, 2)
    slice = append(slice, 3)
    fmt.Printf("len=%d cap=%d\n", len(slice), cap(slice))
    
    slice = append(slice, 4)  // len=4, cap=6 (realloc!)
    fmt.Printf("len=%d cap=%d\n", len(slice), cap(slice))
}

Performance tip: when you know final size, use make([]T, 0, capacity).

// ❌ inefficient: many reallocations
slice := []int{}
for i := 0; i < 10000; i++ {
    slice = append(slice, i)
}

// ✅ preallocate capacity
slice := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    slice = append(slice, i)
}

4. Maps: hash tables

C++ vs Go: maps

#include <unordered_map>
#include <iostream>

int main() {
    std::unordered_map<std::string, int> m;
    
    m["apple"] = 100;
    m["banana"] = 200;
    m.insert({"cherry", 300});
    
    if (m.find("apple") != m.end()) {
        std::cout << "Found: " << m["apple"] << "\n";
    }
    
    int x = m["nonexistent"];  // inserts "nonexistent": 0
    
    m.erase("apple");
    
    for (const auto& [key, value] : m) {
        std::cout << key << ": " << value << "\n";
    }
}
package main

import "fmt"

func main() {
    m := make(map[string]int)
    
    m["apple"] = 100
    m["banana"] = 200
    m["cherry"] = 300
    
    if v, ok := m["apple"]; ok {
        fmt.Println("Found:", v)
    }
    
    x := m["nonexistent"]  // 0, map unchanged
    
    delete(m, "apple")
    
    for key, value := range m {  // order not defined
        fmt.Println(key, ":", value)
    }
}

Key differences:

  • Go maps do not guarantee iteration order
  • Missing keys yield the zero value without inserting (unlike operator[] on unordered_map in the “read missing key” case—Go does not insert)
  • Use the comma-ok form (v, ok := m[k]) to detect presence

Map creation

package main

func main() {
    m1 := make(map[string]int)
    
    m2 := map[string]int{
        "apple":  100,
        "banana": 200,
    }
    
    var m3 map[string]int
    // m3["key"] = 1  // ❌ panic: assignment to entry in nil map
    v := m3["key"]    // ✅ 0, read OK
    
    m4 := make(map[string]int, 100)  // hint ~100 entries
}

Map patterns

package main

import "fmt"

func wordCount(words []string) map[string]int {
    counts := make(map[string]int)
    for _, word := range words {
        counts[word]++
    }
    return counts
}

func uniqueElements(numbers []int) []int {
    seen := make(map[int]bool)
    result := []int{}
    
    for _, num := range numbers {
        if !seen[num] {
            seen[num] = true
            result = append(result, num)
        }
    }
    return result
}

func main() {
    words := []string{"go", "is", "go", "is", "simple"}
    fmt.Println(wordCount(words))
    
    nums := []int{1, 2, 2, 3, 3, 3, 4}
    fmt.Println(uniqueElements(nums))
}

5. Exercises

Exercise 1: reverse a slice in place

package main

import "fmt"

func reverse(slice []int) {
    for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {
        slice[i], slice[j] = slice[j], slice[i]
    }
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    reverse(nums)
    fmt.Println(nums)  // [5 4 3 2 1]
}

C++ comparison: std::reverse(vec.begin(), vec.end()).

Exercise 2: deduplicate

package main

import "fmt"

func removeDuplicates(slice []int) []int {
    seen := make(map[int]bool)
    result := []int{}
    
    for _, v := range slice {
        if !seen[v] {
            seen[v] = true
            result = append(result, v)
        }
    }
    return result
}

func main() {
    nums := []int{1, 2, 2, 3, 3, 3, 4, 5, 5}
    fmt.Println(removeDuplicates(nums))
}

Exercise 3: merge two slices

package main

import "fmt"

func merge(s1, s2 []int) []int {
    result := make([]int, 0, len(s1)+len(s2))
    result = append(result, s1...)
    result = append(result, s2...)
    return result
}

func main() {
    a := []int{1, 2, 3}
    b := []int{4, 5, 6}
    fmt.Println(merge(a, b))
}

Exercise 4: group with a map

package main

import "fmt"

type Student struct {
    Name  string
    Score int
}

func groupByScore(students []Student) map[int][]string {
    groups := make(map[int][]string)
    for _, s := range students {
        groups[s.Score] = append(groups[s.Score], s.Name)
    }
    return groups
}

func main() {
    students := []Student{
        {"Alice", 90},
        {"Bob", 85},
        {"Charlie", 90},
        {"David", 85},
    }
    fmt.Println(groupByScore(students))
}

Wrap-up: Days 3–4 checklist

Goals

  • Go pointers: no arithmetic; dereference only
  • Arguments: by value vs by pointer
  • Arrays are fixed-size; slices are the default tool
  • Understand len, cap, and append growth
  • Slicing shares storage—watch aliasing
  • Maps: construction, lookup, comma-ok
  • Complete the four exercises

C++ → Go cheat sheet

C++GoNotes
int* p; p++p := &x (no ++)Safety first
std::vector<T>[]TSimpler surface syntax
vec.size()len(slice)builtin
vec.capacity()cap(slice)builtin
std::unordered_mapmap[K]Vbuiltin type
m.find(k) != m.end()v, ok := m[k]idiomatic

Next

Next: OOP without classes—composition over inheritance, methods and receivers.


📚 Series navigation

PreviousIndexNext
← #01 Syntax📑 Index#03 OOP →

Go in 2 weeks: Curriculum#01#02#03#04#05#06#07#08#09


TL;DR: Go pointers are safe, slices are powerful, maps are concise—you can do the same jobs as in C++ with less machinery.


Keywords

Go slice, Go map, Go pointer, append len cap, Golang data structures, Go tutorial, C++ vector comparison.

Practical tips

Debugging

  • Start from compiler warnings; reproduce with a minimal case.

Performance

  • Profile before optimizing; define measurable goals.

Code review

  • Align with team conventions; check edge cases and errors.

Field checklist

Before coding

  • Is this the right tool for the problem?
  • Will teammates maintain it?
  • Does it meet performance needs?

While coding

  • Warnings cleared?
  • Edge cases covered?
  • Errors handled?

At review

  • Intent clear?
  • Tests adequate?
  • Docs where needed?

FAQ

Q. Where does this show up in real systems?

A. Understanding vector-vs-slice behavior, safe pointers, and map semantics—exactly what you need for everyday Go services and CLIs.

Q. What should I read first?

A. Follow Previous / Related links at the bottom of each post, or the C++ series index.

Q. Go deeper?

A. Official Go docs and cppreference for C++ parallels.


  • Two-week Go curriculum
  • C++ technical interview questions
  • C++ stack vs heap basics
  • C++ stack vs heap deep dive
  • C++ vs Go