Hi everyone! this is Jimmy, and this is the third article in my series “Breaking Things with Go.” In this series, I document my journey through Jon Bodner’s Second Edition: Learning Go – An Idiomatic Approach to Real-World Go Programming and explore how to use Go in the most practical way I can
in this series, the resources are the book itself, go documentation, and any AI model to clarify some things
lets Jump into it 
Overview
- Pointer is a variable that holds the location in memory where a value is stored
- so the pointer is a value type but its value is another memory address
- every variable is stored in one or more contiguous memory locations called addresses
- different types take different amounts of memory
- smallest amount to store is a byte even if you don't need the entire byte (this behavior raises when we try to store Boolean which needs only one bit but it takes byte)
look for the next example
var x int32 = 10
var y bool = true
pointerX := &x
pointerY := &y
var pointerZ *string
this will be stored in memory as follows
x took 32 bits which is 4 bytes, y took 1 byte
- pointers will always take 4 bytes (doesn't matter what is the type) this is what's needed to store an address (some new programming languages take 8)
- and as you can see the value of the pointer is the starting location of the address where the data was stored
- if the pointer doesn't point to anything it points to nil (this is the zero value for a pointer)
- you can't do arithmetic operations on pointers like we do in c++ (so you can't navigate an array by adding 1 on its base address)
the operators & *
the operator & precedes a value type and returns the address where it was stored
the * operator is the indirection operator
- it precedes variable of pointer type and returns the value it points to
- called dereferencing
make sure the pointer isn't nil before you dereference it or it will panic
if you wanna do full declaration using var you have to specify a pointer type with pointers
the pointer types are written with * before the data type
- so int pointer
var xPointer *intand string*stringand so on
- so int pointer
built-in function
newcreates pointer variable and returns a pointer to zero-value instance of the provided typeto create a pointer type of a struct literal
x := &struct_name{}
the primitive literals like numbers, booleans, strings and constants exist at compile time only (the values themselves not the variable type)
so you can't do this var x int = &45 this is compile time error
but to do this we will use another variable
x := 42
p := &x
what if we have a struct with a primitive types that one of them is a pointer like
type person struct{
FirstName string
MiddleName *string
LastName string
}
p := person{"god", *"damn", "it"} // this is an error
so two ways to do it is to create a variable and pass it inside the struct like
damn := "damn"
damnP := &damn
p := person{"God", damnP, "it"}
second way is to have a helper function which we will pass to the struct field to create the value for us and return the pointer to be stored
func helper[T any](t T) *T {
return &t
}
func main() {
type person struct {
FirstName string
MiddleName *string
LastName string
}
p := person{"God", helper("damn"), "it"}
}
what does the helper functions do ?
[T any]placeholder for any typetakes t of Type T (any type)
returns a pointer of type
*T(if given return*int)&treturn its pointerwhen the function called it will create a copy of the passed argument and stored in memory as a parameter variable (so now it has an address in memory ) then the pointer is returned
Some differences in Python the classes are always pointers so you'll never see a pointer but in Go structs are values by default so using non-pointer structs won't change the values
Pointers indicate mutable Parameters
- Go doesn't have a keyword to identify immutable variables, structs, or fields
- constant as we said compile-time constant that has no memory address
- so what if we need to identify an immutable variable ?
- if you need something to be immutable just use it as value not pointer
but you still can change it right ? yes but look at this MIT quote about mutation
Actually, using mutable objects is just fine if you are using them entirely locally within a method, and with only one reference to the object
so what we are trying to do is making it immutable to other function which is what GO guarantees cause functions receive a copy of data so unless this data is a pointer the data is considered immutable
Passing pointer to a function
when you pass a pointer, Go copies the pointer value this values is either an address or nil the function gets its own copy of that value look at this example to understand
func failedUpdate(g *int){
x := 10
g = &x
}
func main(){
var f *int
failedUpdate(f)
fmt.Println(f)
}
this won't change F here is why we create a pointer that points to nil and got a copy of that pointer and we changed the value of copy pointer to point to 10 but the original pointer still points to nil

now if you need to change the original value you have to dereference the pointer because dereferencing puts the new value in the memory location pointed to by both the original and the copy
func update(px *int) {
*px = 20
}
func main() {
x := 10
update(&x)
fmt.Println(x)
}

Pointers are Last resort
pointers are harder to understand and create extra work for the garbage collector
so don't pass a struct pointer to a function to modify its value instead make the struct inside that function and then return it

Pointer Passing Performance
if the struct is large enough using a pointer to the struct is better why ?
- cause no matter the data size is the pointer is constant size so it takes like 1 nanosecond
if you pass large struct as value it takes .7 millisecond once the value gets to be around 10 megabytes but if the data is small ? there is interesting behavior
- if data is less than 10 megabytes → the values takes less time than pointer to be returned
- but when the data gets larger the situation is reversed
Zero value vs no value
pointers are used to indicate the difference between variable or field that's been assigned to zero value or field with no value at all
- if we need that behavior use nil pointer for fields without value at all
care passing nil pointer to function makes you unable to modify the value that the pointer points to
pointers mean mutability so if you don't need that use the comma, ok idiom to check if the value exists or not
Difference between slices and maps
Maps
- any modification on a map passed to a function will reflect on the original map
- that's because maps are implemented as a pointer to a struct
- passing the map to the function means passing the pointer and here is a quick example on that
func edit(p map[string]string) {
p["Name"] = "Ahmed"
}
func main() {
person := map[string]string{
"Name": "jimmy",
"Team": "Lakers",
"Fav_Player": "Luka Donicic",
}
edit(person)
fmt.Println(person) // map[Fav_Player:Luka Donicic Name:Ahmed Team:Lakers]
}
Slices
- the slice is a little bit trickier than maps we know that the slice consists of 3 things
- backing array, Length, Capacity
if we used slice in the same function
- trying to append items we will check the capacity with the length if there is enough space we will add the items
- if the length is equal to the capacity we will create a new backing array and point to the new one instead
if the slice is passed to a function the behavior changes a little bit
any modification to the slice's content will reflect to the original one
trying to append items will be reflected in the original one but the length won't be changed so the Go runtime will prevent original slice from reading beyond the length

trying to append more than the capacity will create a new backing array but it won't be pointed to be the original one

so the only way to reflect those modifications into the original one is if the slice has 4 items you change any one of those items only
func my_append(num []int) {
num = append(num, 5)
}
func over_append(num []int) {
num = append(num, 3, 4, 2, 4, 5, 6, 7)
num[0] = 2
}
func edit(num []int) {
num[0] = 2
}
func main() {
numbers := make([]int, 4, 6)
my_append(numbers)
fmt.Println(numbers) // any changes after the passed len won't be reflected
over_append(numbers)
fmt.Println(numbers) // created a new backing array separated from each other, you can't even modify before the length now
edit(numbers)
fmt.Println(numbers) // changes will be reflected
}
we can write a slice that takes a slice of any size but we can't create a function that takes array of any size cause each size has its own type
Slices as Buffers
Problem: Traditional data reading patterns create unnecessary memory allocations in loops, generating excessive garbage for the collector to clean up.
Go's Solution: Use a single pre-allocated slice as a reusable buffer:
data := make([]byte, 100)
for {
count, err := file.Read(data)
process(data[:count])
// Handle errors...
}
This approach reuses the same memory buffer across iterations, minimizing allocations.
The Stack
- Fast, simple consecutive memory block
- Shared by all function calls in a thread
- Allocation is just moving a pointer
- Local variables with known compile-time sizes live here
- Memory automatically deallocated when function exits
The Heap
- Managed by garbage collector
- Used when data size is unknown at compile time
- Used when pointers to local variables are returned
- Much slower to allocate and access than stack
Escape Analysis
Go's compiler determines whether data can stay on the stack or must "escape" to the heap. Data escapes when:
- Size is unknown at compile time
- Pointer to local variable is returned from function
- Compiler cannot guarantee stack safety
the rest of the chapter I don't care about. some garbage collection and optimization that we can read about later if we needed to(if you are a software engineer you should be interested in that section)
Takes
- Minimize allocations: Reuse buffers instead of creating new ones
- Prefer stack over heap: Use value types when possible
- Avoid excessive pointers: Keep data sequential in memory
Coming Next
the next break will cover the next chapter which is types, methods, and interfaces (interesting stuff) so stick around for next the break
feel free to reach out to me