Site Logo
Niklas Heringer - Cybersecurity Blog
Cover Image

More to Go: Clean Code & Core Concepts

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? var declares 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

prices now looks like {2.99, 5.99, 5.99}, empty_arr consists of only zeroes and the non-initialized values of half_empty_arr are 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 Image 🛠 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:

🔍 Slice Creation: 3 Main Ways

1. Literal Declaration

myslice := []int{1, 2, 3}

If empty:

myslice := []int{}

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]

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)
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:

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 index a (inclusive) to b (exclusive) - so a’s in but b’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] 

🧠 You don’t get a new array — just a pointer to part of it.

🏗️ Slices with make()

s := make([]int, 5, 10)
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’ll append() later

Why use make() over literal?

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:

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 break needed — 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)
}

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 break to stop work early 📌 Use continue to 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 range over slices/arrays is a bit slower than a classic indexed for, 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


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?

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 *Person if 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 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 is nil → 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 ok to 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?

Until next time!