 
  
    
    
    
 
  
More to Go: Clean Code & Core Concepts
Table of Contents
We’re baack
Let’s get into the real Go deal with this one shall we hehe.
📦 Go Arrays
Arrays in Go can either be declared via the var keyword or our beloved walrus operator :=:
var <ARRAY_NAME> = [length]datatype{values}
# with inferred length:
var <ARRAY_NAME> = [...]datatypes{values}
does the same as
<ARRAY_NAME> := [length]datatype{values}
# with inferred length:
<ARRAY_NAME> := [...]datatypes{values}
Remember the difference?
vardeclares the variables with explicit type or zero-value and is (thereby) usable outside functions, directly on package level. The walrus operator:=can only be used inside functions and the type is compiler-inferred.
Arrays: Examples & Operations
package main
import f "fmt"
func main() {
	prices := [3]float32{2.99, 5.99, 8.99}
	empty_arr := [5]int{}          // seems not initialized?
	half_empty_arr := [5]int{1, 2} // seems partially initialized
	prices[2] = prices[1]
	f.Println(prices[2])
	f.Println(empty_arr)
	f.Println(half_empty_arr[3])
}
What do you think is printed?
5.99
[0 0 0 0 0]
0
pricesnow looks like{2.99, 5.99, 5.99},empty_arrconsists of only zeroes and the non-initialized values ofhalf_empty_arrare also filled with zeroes.
Specific Partial Array Initializations
Interestingly, you can also do:
weird_array := [5]int{1:10,2:40}
f.Println(weird_array)
[0 10 40 0 0]
works with a nice concept of <ARR_POSITION>:<VALUE>.
len()works just as in Python,len(weird_array) = 5.
☕ Magic lies ahead of you..
Before the real magic begins, for a moment consider supporting me in my endless pursuit of free educational content :D
  
  
  
  
     🛠 Every coffee helps fuel more content like this — cheatsheets, walkthroughs, and more hacker-fueled documentation.
  
🛠 Every coffee helps fuel more content like this — cheatsheets, walkthroughs, and more hacker-fueled documentation.
🔗 Visit: ko-fi.com/niklasheringer
🍕 Go Slices - ArrayList goes brr
Java connoisseurs will love this one.
🧠 What is a Slice in Go?
A slice is a flexible, dynamic view into an array. Think of it like a window into an underlying array.
It has:
- len()→ How many elements are in the window.
- cap()→ How many elements the window could stretch to, without reallocating.
🔍 Slice Creation: 3 Main Ways
1. Literal Declaration
myslice := []int{1, 2, 3}
- len = 3,- cap = 3
- Backed by a hidden array of 3 ints.
If empty:
myslice := []int{}
- len = 0,- cap = 0
Great for: small static lists, or passing data inline.
2. Slice from an Array
arr := [6]int{10, 11, 12, 13, 14, 15}
myslice := arr[2:4]
- myslice = [12, 13]
- len = 2(elements 2 and 3)
- cap = 4(from index 2 → end of array)
Key concept:
Capacity = distance from start index to end of backing array.
📌 If we sliced from arr[0:4] → cap = 6
Great for: controlled slicing, memory views, performance work.
  3. Using make()
  
  
    
 
  
myslice := make([]int, 5, 10)
- len = 5,- cap = 10
- Backed by array of 10 ints, but only first 5 “active”.
myslice := make([]int, 5) // capacity = length = 5
Great for: preallocating memory, better performance in loops/appends.
To summarize
| Method | Syntax Example | Length | Capacity | Notes | 
|---|---|---|---|---|
| Literal | []int{1,2,3} | 3 | 3 | Short and clean | 
| Empty literal | []int{} | 0 | 0 | Empty but valid | 
| From array | arr[2:4] | 2 | arr.len-2 | Capacity depends on start index | 
| make()(full) | make([]int, 5, 10) | 5 | 10 | Ideal for performance setups | 
| make()(short) | make([]int, 5) | 5 | 5 | Cleaner, less config | 
Remember:
- You can grow a slice with append()up to its capacity without reallocating.
- Slices are references: modifying a slice affects the underlying array.
- Use make()when performance or growth matters.
- Use slicing when working with large arrays efficiently.
Slices are just views into arrays — cheap and powerful, but watch out for hidden memory costs!
Slice elements…
… Accessing and modifying
slice := []int{10, 20, 30}
fmt.Println(slice[0]) // access
slice[2] = 50          // modify
… Appending Elements
slice = append(slice, 40, 50)
📌 If capacity is exceeded, Go allocates a new underlying array.
… Append One Slice to Another
combined := append(slice1, slice2...)
… Change Slice Length via Reslicing
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:4] // picks index 1 to 3 → [20, 30, 40]
📌
slice[a:b]gives a view from indexa(inclusive) tob(exclusive) - soa’s in butb’s not. Underlying array remains the same! No new memory yet.
  🔍 Behind the scenes of slice[a:b]
  
  
    
 
  
array := [5]int{10, 20, 30, 40, 50}
slice := array[1:4] 
- slicepoints to- array[1]
- len(slice) = 3(- b-- a)
- cap(slice) = 4(from- ato end of array)
🧠 You don’t get a new array — just a pointer to part of it.
  🏗️ Slices with make()
  
  
    
 
  
s := make([]int, 5, 10)
- len = 5→ number of usable elements
- cap = 10→ total allocated backing array size
- All elements default to zero values
s[0] = 99 // works, since len = 5
s[5] = 100 // ❌ panic: index out of range (len is still 5)
make()allocates a real backing array and gives you a slice pointing to it. Efficient for preallocation — useful when you’llappend()later
  Why use make() over literal?
  
  
    
 
  
- If you know the length + future growth, make()avoids unnecessary reallocations
- Especially useful in loops or buffer-heavy operations
buffer := make([]byte, 0, 1024) // len=0, cap=1024
✅ Fast, efficient, appendable
🧮 Operators in Go
Arithmetic Operators
Used for basic math operations:
| Operator | Description | Example | 
|---|---|---|
| + | Addition | a + b | 
| - | Subtraction | a - b | 
| * | Multiplication | a * b | 
| / | Division | a / b | 
| % | Modulo (remainder) | a % b | 
Assignment Operators
Used to assign values to variables:
| Operator | Description | Example | 
|---|---|---|
| = | Assign value | a = 5 | 
| += | Add and assign | a += 3 | 
| -= | Subtract and assign | a -= 2 | 
| *= | Multiply and assign | a *= 4 | 
| /= | Divide and assign | a /= 2 | 
| %= | Modulo and assign | a %= 3 | 
Comparison Operators
Used to compare values:
| Operator | Description | Example | 
|---|---|---|
| == | Equal to | a == b | 
| != | Not equal to | a != b | 
| > | Greater than | a > b | 
| < | Less than | a < b | 
| >= | Greater than or equal to | a >= b | 
| <= | Less than or equal to | a <= b | 
Logical Operators
Welp — can’t make this a table here, because | characters end tables in Markdown and mess everything up. So here’s the breakdown in plain text instead:
  && – Logical AND
  
  
    
 
  
Used when both conditions must be true. Example:
if a > 0 && b < 5 {
    // do something
}
  || – Logical OR
  
  
    
 
  
Used when either condition can be true. Example:
if a == 0 || b > 5 {
    // do something
}
  ! – Logical NOT
  
  
    
 
  
Used to invert a condition. Example:
if !(a == b) {
    // do something if a is NOT equal to b
}
These work exactly like in other C-style languages, and combine cleanly with
if,switch, or even ternary-like logic (Go-style).
Bitwise Operators
Used at the bit level:
| Operator | Description | Example | ||
|---|---|---|---|---|
| & | AND | a & b | ||
| ` | ` | OR | `a | b` | 
| ^ | XOR (exclusive OR) | a ^ b | ||
| &^ | AND NOT (bit clear) | a &^ b | ||
| << | Left shift | a << 1 | ||
| >> | Right shift | a >> 1 | 
🧪 Go Conditions – Nothing Fancy
Yep, it’s the standard stuff:
- Comparison operators: <,<=,>,>=,==,!=
- Logical operators: &&(and),||(or),!(not)
Control Flow
  if, else if, else
  
  
    
 
  
if x > y {
    // do something
} else if x == y {
    // do something else
} else {
    // fallback
}
Nested
if’s also work like you’d expect:
if x > 0 {
    if x < 100 {
        fmt.Println("x is between 1 and 99")
    }
}
  switch (single-case) is just like in other languages:
  
  
    
 
  
switch day {
case "Mon":
    // ...
case "Tue":
    // ...
default:
    // ...
}
  Go Multi-case-switch
  
  
    
 
  
You can group multiple cases with commas:
switch day {
case 1, 3, 5:
    fmt.Println("Odd weekday")
case 2, 4:
    fmt.Println("Even weekday")
case 6, 7:
    fmt.Println("Weekend")
default:
    fmt.Println("Invalid day")
}
Clean, readable, no
breakneeded — Go handles that for you.
🔁 Go Loops – Performance, Memory & Control
Go keeps loops simple and efficient — but you need to be mindful of memory and speed, especially in large data processing.
  Classic for Loop – Fast & Predictable
  
  
    
 
  
for i := 0; i < 10; i++ {
    fmt.Println(i)
}
- Best when you need index control
- Efficient when counter and conditions are simple
- Ideal for bounded iteration
  ✅ continue and break
  
  
    
 
  
for i := 0; i < 5; i++ {
    if i == 3 {
        continue // skip 3
    }
    if i == 4 {
        break // exit early
    }
    fmt.Println(i)
}
📌 Use
breakto stop work early 📌 Usecontinueto skip unnecessary iterations (perf gain)
Nested Loops – Caution!
for i := 0; i < len(adj); i++ {
    for j := 0; j < len(fruits); j++ {
        fmt.Println(adj[i], fruits[j])
    }
}
⚠️ Each level multiplies runtime complexity. 🔍 Be sure to avoid deep nesting over large datasets — can slow performance and hog CPU.
  range – Clean, but Benchmark It
  
  
    
 
  
for idx, val := range slice {
    fmt.Println(idx, val)
}
✅ Readable ✅ Automatically handles index/value
But
rangeover slices/arrays is a bit slower than a classic indexedfor, especially in tight performance loops.
You can skip unneeded parts:
for _, val := range slice { ... } // skip index
for idx := range slice { ... }   // skip value
Memory-Aware Looping
DO:
  Preallocate slices with make() when appending in a loop:
  
  
    
 
  
result := make([]int, 0, 1000) // avoids reallocation
Reuse buffers where possible:
buffer := make([]byte, 1024)
DON’T:
Keep growing slices blindly — this causes repeated allocations + copies:
for i := 0; i < bigN; i++ {
    growing = append(growing, i) // ⚠️ inefficient if not preallocated
}
⚙️ Looping Over Large Data Sets
- Use plain for i := 0when you care about speed.
- Use rangewhen you care about clarity.
- Use copy()or slice preallocation to avoid dragging unused memory.
More to Functions – Power Tools in Go
Go functions are simple to start with, but there’s a lot more under the hood. Here’s a compact yet clear breakdown of parameters, returns, multiple values, and recursion — with performance/memory tips along the way.
Function Parameters
You can pass data into functions via parameters:
func greet(name string) {
    fmt.Println("Hello", name)
}
Call it like:
greet("Elliot")
📌 Parameters are copied by value, unless you’re working with pointers or reference types like slices/maps.
Multiple Parameters
func userInfo(name string, age int) {
    fmt.Println(name, "is", age, "years old")
}
Arguments must match order and type.
Return Values
Functions can return values:
func add(x int, y int) int {
    return x + y
}
sum := add(2, 3)
Named Return Values
You can name return variables:
func add(x, y int) (result int) {
    result = x + y
    return
}
📌 This makes the code self-documenting, but overusing it can reduce clarity.
Multiple Return Values
func process(x int, y string) (int, string) {
    return x * 2, y + "!"
}
Call with unpacking:
a, b := process(5, "Go")
🧠 Use
_to ignore unused return values:
_, msg := process(1, "skip")
Store Return in Variable
total := add(3, 4)
fmt.Println(total)
Good for reusing results and improving readability.
Recursion
Go supports recursion — functions that call themselves:
Basic recursion:
func countUp(x int) {
    if x == 5 {
        return
    }
    fmt.Println(x)
    countUp(x + 1)
}
Factorial example:
func factorial(x int) int {
    if x == 0 {
        return 1
    }
    return x * factorial(x - 1)
}
⚠️ Recursion uses stack memory — easy to blow up with deep or infinite recursion.
Prefer iteration over recursion for performance unless recursion is clearer (e.g., trees, divide-and-conquer algorithms).
🧠 Performance & Memory Tips
| Case | Tip | 
|---|---|
| Many parameters | Pass structs or pointers instead | 
| Repeated return values | Use var, store once — avoid recomputation | 
| Recursion with large input | Watch for stack overflow | 
| Large results (slices/maps) | Consider returning pointers | 
🧱 Go Structs – Organizing Data the Go Way
Structs in Go let you group related data into a single object — like a lightweight, type-safe record. Super handy for modeling real-world entities, API data, config, etc.
What is a Struct?
- Arrays = same type
- Structs = different types
- Think of them like “mini objects” without methods (unless you add them later).
Declaring a Struct
type Person struct {
    name   string
    age    int
    job    string
    salary int
}
This defines a blueprint for what a Person looks like.
Creating & Using Structs
var p1 Person
p1.name = "Hege"
p1.age = 45
p1.job = "Teacher"
p1.salary = 6000
fmt.Println(p1.name) // Hege
You can also use composite literals to build structs faster:
p2 := Person{
    name:   "Cecilie",
    age:    24,
    job:    "Marketing",
    salary: 4500,
}
📌 Order matters if you omit field names — so prefer named fields for clarity.
Passing Structs to Functions
func printPerson(p Person) {
    fmt.Println("Name:", p.name)
}
You can pass structs by value (copy) or by reference (pointer):
func updateSalary(p *Person, amount int) {
    p.salary += amount
}
📌 Use
*Personif you want the function to modify the original struct.
Why Structs Matter
| Use Case | Why Structs Rock | 
|---|---|
| Grouping related fields | Clean, readable, type-safe | 
| Function input/output | Avoids long param lists, documents intent | 
| Modeling real-world data | Natural fit for configs, JSON, etc. | 
🧠 Memory Tips
Structs are value types — copied on assignment
a := b // a is a copy
  Pass pointers (*Struct) if:
  
  
    
 
  
- You want to modify the original
- Struct is large (performance gain)
You can nest structs:
type Address struct {
    city string
}
type User struct {
    name string
    addr Address
}
🗺️ Go Maps – Key:Value Power
Maps in Go let you store and retrieve key-value pairs efficiently — like dictionaries in other languages. They’re unordered, mutable, and reference-based.
Declaring Maps
a := map[string]string{"brand": "Ford", "model": "Mustang"}
Or using make():
b := make(map[string]int)
b["Oslo"] = 1
📌
make()is preferred when building maps dynamically (zero value isnil→ writing to it = panic!).
Access, Add, Update
fmt.Println(a["brand"])   // Access
a["year"] = "1970"        // Add or Update
Delete Keys
delete(a, "year")
Check Key Existence
val, ok := a["color"]
if ok {
    fmt.Println("Exists:", val)
}
📌 Use
okto distinguish missing keys from keys with empty values.
Maps Are References
b := a
b["year"] = "2000"
// a and b both see the change
🧠 Modifying one affects the other unless you deep copy.
Iterate Over Maps
for k, v := range a {
    fmt.Println(k, v)
}
⚠️ Unordered — if you want a specific order, use a separate slice:
keys := []string{"brand", "model"}
for _, k := range keys {
    fmt.Println(k, a[k])
}
🧠 Performance & Memory Tips
| Task | Efficient Approach | 
|---|---|
| Pre-building big maps | Use make(map[Type]Type, capacity) | 
| Clearing a map | for k := range m { delete(m, k) } | 
| Avoiding collisions | Choose good key types (short strings, ints) | 
| Copying maps | Manual copy only – no built-in deep copy | 
🧾 Wrap-Up: Go Core Concepts, Cleanly Delivered
You now have a clean, focused understanding of Go fundamentals:
| Topic | You Learned | 
|---|---|
| ✅ Variables | var,:=, type inference, scope | 
| ✅ Slices | Length, capacity, append(), memory handling | 
| ✅ Conditions | if,else,else if,switch, nested logic | 
| ✅ Loops | for,range,break,continue, efficiency tips | 
| ✅ Functions | Params, returns, named returns, recursion | 
| ✅ Structs | Modeling data, passing to functions, memory safety | 
| ✅ Maps | Key-value storage, reference behavior, iteration | 
🧠 Next Steps?
- Practice writing simple CLI tools using these concepts
- Add tests (_test.go) to build muscle memory
- Dive into methods, interfaces, and concurrency (goroutines, channels) when you’re ready
Until next time!