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

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

이 글의 핵심

Go pointers, slices, and maps for C++ developers: safe *T, len/cap/append, map lookup with ok, and how slices differ from std::vector. Part of the 2-week Go series.

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

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// C++: nullptr
int* p = nullptr;
if (p == nullptr) {
    std::cout << "null pointer\n";
}
// Dereference → segfault
// *p = 10;  // crash!

아래 코드는 go를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

아래 코드는 go를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// A slice is a small struct of three fields
type slice struct {
    ptr *[...]T  // pointer to backing array
    len int      // length
    cap int      // capacity
}

아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

graph LR
    A[Slice] --> B["ptr: array pointer"]
    A --> C["len: length"]
    A --> D["cap: capacity"]
    B --> E[Backing array memory]

Creating slices

다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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). 아래 코드는 go를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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";
    }
}

다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 패키지 선언
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

다음은 go를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3