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
- Pointers: no arithmetic, but dereference works
- Arrays: fixed-size value types
- Slices: Go’s dynamic arrays
- Maps: hash tables
- 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]intand[10]intdiffer - 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[]onunordered_mapin 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, andappendgrowth - Slicing shares storage—watch aliasing
- Maps: construction, lookup, comma-ok
- Complete the four exercises
C++ → Go cheat sheet
| C++ | Go | Notes |
|---|---|---|
int* p; p++ | p := &x (no ++) | Safety first |
std::vector<T> | []T | Simpler surface syntax |
vec.size() | len(slice) | builtin |
vec.capacity() | cap(slice) | builtin |
std::unordered_map | map[K]V | builtin type |
m.find(k) != m.end() | v, ok := m[k] | idiomatic |
Next
Next: OOP without classes—composition over inheritance, methods and receivers.
📚 Series navigation
| Previous | Index | Next |
|---|---|---|
| ← #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.
Related reading
- [Go #01] Philosophy & syntax
- Two-week Go curriculum
- C++ stack vs heap guide
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.
Related posts
- Two-week Go curriculum
- C++ technical interview questions
- C++ stack vs heap basics
- C++ stack vs heap deep dive
- C++ vs Go