

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?
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 ofhalf_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
🛠 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]
slice
points toarray[1]
len(slice) = 3
(b
-a
)cap(slice) = 4
(froma
to 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 elementscap = 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
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)
}
- 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
break
to stop work early 📌 Usecontinue
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 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 := 0
when you care about speed. - Use
range
when 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
*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 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
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?
- 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!