Arrays — Tasks¶
Junior Tasks¶
Task 1: Basic Array Operations¶
- Type: Implementation
- Goal: Practice declaring, initializing, and iterating over arrays.
Starter Code:
package main
import "fmt"
func main() {
// TODO 1: Declare a [5]int array named "scores"
// and initialize it to: 85, 92, 78, 96, 88
// TODO 2: Print the array
// TODO 3: Calculate and print the sum of all scores
// TODO 4: Find and print the highest score
// TODO 5: Count how many scores are >= 90 and print the count
}
Expected Output:
Evaluation Criteria: - Correct array declaration syntax with values - Proper use of range for iteration - Correct sum calculation - Correct maximum finding algorithm - Correct conditional counting
Task 2: Array as Value Type¶
- Type: Conceptual + Implementation
- Goal: Demonstrate understanding of array copy semantics.
Starter Code:
package main
import "fmt"
// modifyFirst should NOT affect the original (receives a copy)
func modifyFirst(arr [5]int) {
// TODO: set arr[0] = 99 and print the arr
}
// modifyFirstByPointer SHOULD affect the original
func modifyFirstByPointer(arr *[5]int) {
// TODO: set arr[0] = 99 and print the arr
}
func main() {
original := [5]int{1, 2, 3, 4, 5}
fmt.Println("Before modifyFirst:", original)
modifyFirst(original)
fmt.Println("After modifyFirst:", original) // should be unchanged
fmt.Println("Before modifyFirstByPointer:", original)
modifyFirstByPointer(&original)
fmt.Println("After modifyFirstByPointer:", original) // should be changed
}
Expected Output:
Before modifyFirst: [1 2 3 4 5]
inside modifyFirst: [99 2 3 4 5]
After modifyFirst: [1 2 3 4 5]
Before modifyFirstByPointer: [1 2 3 4 5]
inside modifyFirstByPointer: [99 2 3 4 5]
After modifyFirstByPointer: [99 2 3 4 5]
Evaluation Criteria: - Correct function signatures (value vs pointer parameter) - Understanding that value argument is a copy - Correct use of pointer dereference when needed
Task 3: Multi-Dimensional Array (Tic-Tac-Toe)¶
- Type: Implementation
- Goal: Work with a 2D array to simulate a game board.
Starter Code:
package main
import "fmt"
const size = 3
func initBoard(board *[size][size]string) {
// TODO: fill every cell with "."
}
func printBoard(board [size][size]string) {
// TODO: print each row, space-separated
}
// checkWin returns true if any row, column, or diagonal is all "X"
func checkWin(board [size][size]string) bool {
// TODO: implement
return false
}
func main() {
var board [size][size]string
initBoard(&board)
printBoard(board)
board[0][0] = "X"
board[1][1] = "X"
board[2][2] = "X"
fmt.Println("\nAfter moves:")
printBoard(board)
fmt.Println("X wins:", checkWin(board))
}
Expected Output:
Evaluation Criteria: - Correct 2D array traversal with nested loops - Pointer parameter usage for board initialization - Win detection checks all rows, columns, and both diagonals
Middle Tasks¶
Task 4: Fixed-Size Ring Buffer¶
- Type: Data Structure Implementation
- Goal: Implement a fixed-capacity ring buffer using an array.
Starter Code:
package main
import (
"errors"
"fmt"
)
const bufferSize = 8
type RingBuffer struct {
data [bufferSize]int
head int // index of next read
tail int // index of next write
count int
}
var ErrFull = errors.New("buffer full")
var ErrEmpty = errors.New("buffer empty")
func (r *RingBuffer) Push(v int) error {
// TODO: return ErrFull if count == bufferSize
// TODO: store v at data[tail], advance tail with wrap-around
return nil
}
func (r *RingBuffer) Pop() (int, error) {
// TODO: return 0, ErrEmpty if count == 0
// TODO: read data[head], advance head with wrap-around
return 0, nil
}
func (r *RingBuffer) Len() int { return r.count }
func main() {
rb := &RingBuffer{}
for i := 1; i <= bufferSize; i++ {
rb.Push(i * 10)
}
fmt.Println("Len:", rb.Len()) // 8
if err := rb.Push(999); err != nil {
fmt.Println("Expected:", err)
}
for i := 0; i < 4; i++ {
v, _ := rb.Pop()
fmt.Println("Popped:", v)
}
fmt.Println("Len after pops:", rb.Len()) // 4
}
Expected Output:
Evaluation Criteria: - Correct wrap-around with modulo arithmetic - FIFO ordering preserved - Correct full/empty detection - No off-by-one errors
Task 5: Protocol Header Parser¶
- Type: System Programming
- Goal: Parse a fixed-size binary protocol header using a typed array.
Starter Code:
package main
import (
"encoding/binary"
"fmt"
)
// 12-byte header layout:
// [0] Version uint8
// [1] MessageType uint8
// [2:4] Flags uint16 big-endian
// [4:8] SequenceNum uint32 big-endian
// [8:12] PayloadLen uint32 big-endian
type Header [12]byte
func (h Header) Version() uint8 { return h[0] }
func (h Header) MessageType() uint8 { return h[1] }
// TODO: implement Flags() uint16
// TODO: implement SequenceNum() uint32
// TODO: implement PayloadLen() uint32
// TODO: implement BuildHeader(ver, msgType uint8, flags uint16, seq, payloadLen uint32) Header
func main() {
h := BuildHeader(1, 5, 0x0003, 42, 256)
fmt.Printf("Version: %d\n", h.Version())
fmt.Printf("MessageType: %d\n", h.MessageType())
fmt.Printf("Flags: 0x%04X\n", h.Flags())
fmt.Printf("SequenceNum: %d\n", h.SequenceNum())
fmt.Printf("PayloadLen: %d\n", h.PayloadLen())
}
Expected Output:
Evaluation Criteria: - Correct use of binary.BigEndian for multi-byte fields - Methods have value receivers - BuildHeader writes all bytes correctly
Task 6: Benchmark Array Pass by Value vs Pointer¶
- Type: Performance Analysis
- Goal: Measure and explain the cost difference.
Starter Code:
package main_test
import "testing"
func sumByValue(arr [1000]int) int {
sum := 0
for _, v := range arr { sum += v }
return sum
}
func sumByPointer(arr *[1000]int) int {
sum := 0
for _, v := range arr { sum += v }
return sum
}
// TODO: Write BenchmarkByValue using b.ReportAllocs()
// TODO: Write BenchmarkByPointer using b.ReportAllocs()
// TODO: Add a comment analyzing the results
Expected Output (approximate):
BenchmarkByValue-8 500000 2400 ns/op 8192 B/op 1 allocs/op
BenchmarkByPointer-8 2000000 600 ns/op 0 B/op 0 allocs/op
Evaluation Criteria: - Correct benchmark signatures - Use of b.ResetTimer() and b.ReportAllocs() - Written comment analyzing the performance difference
Senior Tasks¶
Task 7: Cache-Line-Padded Concurrent Histogram¶
- Type: Concurrent Systems
- Goal: Implement a high-throughput histogram that scales with core count.
Starter Code:
package main
import (
"fmt"
"sync"
"sync/atomic"
"unsafe"
)
const cacheLineSize = 64
// TODO: Define paddedCounter with int64 value + padding to reach 64 bytes
// TODO: Define Histogram struct with [16]paddedCounter
// TODO: Record(bucket int) — atomic increment
// TODO: Total() int64 — sum all buckets
// TODO: BucketCount(bucket int) int64
func main() {
hist := &Histogram{}
var wg sync.WaitGroup
for g := 0; g < 16; g++ {
wg.Add(1)
go func(bucket int) {
defer wg.Done()
for i := 0; i < 100000; i++ {
hist.Record(bucket)
}
}(g)
}
wg.Wait()
fmt.Println("Total:", hist.Total()) // 1600000
_ = unsafe.Sizeof(paddedCounter{}) // should be 64
}
Expected Output:
Evaluation Criteria: - sizeof(paddedCounter) == 64 - Atomic operations, no mutex on hot path - No data races (go test -race) - Scales with GOMAXPROCS
Task 8: Zero-Allocation Hash-Keyed Cache¶
- Type: System Design + Performance
- Goal: Cache with
[32]bytekeys and zero per-lookup allocation.
Starter Code:
package main
import (
"crypto/sha256"
"fmt"
"sync"
)
type Cache struct {
mu sync.RWMutex
entries map[[32]byte][]byte
}
func NewCache() *Cache {
return &Cache{entries: make(map[[32]byte][]byte)}
}
// TODO: Get(input []byte) ([]byte, bool)
// - sha256.Sum256 result is used directly as map key (no string alloc)
// TODO: Set(input []byte, value []byte)
func main() {
c := NewCache()
c.Set([]byte("hello"), []byte("world"))
c.Set([]byte("foo"), []byte("bar"))
v, ok := c.Get([]byte("hello"))
fmt.Println(ok, string(v)) // true world
v, ok = c.Get([]byte("missing"))
fmt.Println(ok, v) // false []
}
Expected Output:
Evaluation Criteria: - sha256.Sum256 result used as map key without string conversion - Correct mutex usage (RLock for Get, Lock for Set) - No data races
Questions¶
Q1: What is printed by:
Answer:99 — the slice s shares the underlying array with a. Q2: Which causes a compile error?
Answer: Compile error —[3]int and [4]int are different types and cannot be compared. Q3: What is unsafe.Sizeof([0]int{})? Answer: 0 — zero-size arrays take zero bytes.
Q4: Why is for _, v := range arr { v = 99 } ineffective? Answer: v is a copy of each element. Use for i := range arr { arr[i] = 99 } instead.
Q5: How do you pass arr [5]int to a function expecting []int? Answer: f(arr[:]) — the slice expression arr[:] creates a slice view over the array.
Mini Projects¶
Mini Project 1: 8x8 RGBA Pixel Buffer¶
Build a simple 8x8 image as [8][8][4]uint8 (R, G, B, A channels). Implement: - NewImage() — blank white image - SetPixel(x, y int, r, g, b, a uint8) - GetPixel(x, y int) (r, g, b, a uint8) - DrawBorder(r, g, b uint8) — set all edge pixels to given color - Print() — ASCII representation using W (white) and . (dark)
Mini Project 2: SHA256 File Deduplicator¶
Build a tool that: 1. Accepts a list of file paths as []string 2. Computes sha256.Sum256 for each file 3. Groups files by hash using map[[32]byte][]string 4. Reports which files are duplicates
Challenge¶
Challenge: Generic Fixed-Size Stack with Benchmarks¶
Implement a generic stack backed by a fixed-size array:
type Stack[T any] struct {
data [16]T
top int
}
func (s *Stack[T]) Push(v T) bool // false if full
func (s *Stack[T]) Pop() (T, bool) // false if empty
func (s *Stack[T]) Peek() (T, bool) // false if empty
func (s *Stack[T]) Len() int
func (s *Stack[T]) IsFull() bool
func (s *Stack[T]) IsEmpty() bool
Requirements: 1. LIFO ordering verified by tests 2. No heap allocation per Push/Pop (use -benchmem to verify) 3. Thread-safe wrapper using sync.Mutex 4. Benchmark comparing locked vs lockless versions 5. 100% test coverage (empty pop, full push, interleaved ops)