Learning Go: My Weekend Deep Dive
From the official tour to building concurrent backends—my journey into Go (Golang) and why I'm using it for my next SaaS IIoT project.
Last weekend, I decided to step out of my comfort zone and dive into Go (Golang). As I prepare to build the backend for my new SaaS IIoT (Industrial Internet of Things) project, I needed a language that could handle high concurrency and provide robust performance without the overhead of heavier runtimes. Go seemed like the perfect candidate.
The Learning Path: The Official Go Tour
I started where most Gophers begin: the Official Go Tour. It’s a fantastic, interactive way to get hands-on with the syntax immediately. But what really stuck with me was how Go compares to the languages I use daily: TypeScript and Python.
1. The Basics: Go vs. Python & TypeScript
Coming from a TypeScript/Python background, Go's strict typing and C-style syntax felt like a hybrid.
- Variables & Types: unlike Python's dynamic typing, Go is statically typed like TypeScript. However, the type inference (
:=) makes it feel almost as concise as Python. - Compilation: Go compiles directly to machine code. There's no VM like the JVM or interpreter like Python. This results in incredibly fast startup times—crucial for my IoT lambda functions.
- Loops: Go has only one looping construct:
for. Nowhile, nodo-while. You just useforin different ways. This minimalism is a stark contrast to the syntactic sugar I'm used to in JS/TS (like.map,.filter,.reduce), though Go is slowly adopting some of these functional patterns.
2. Structs, Methods, and Interfaces
This is where Go gets interesting. It’s object-oriented but not in the traditional class-based sense I'm used to in Python or ES6 classes.
- Structs vs Classes: In Python, I define a
class. In Go, I define astructto hold data. It feels very similar to a TypeScriptinterfaceortypedefinition, but it exists at runtime. - Methods: You don't put methods inside the struct. You define functions with a receiver. This decoupling of data and behavior was a paradigm shift.
- Implicit Interfaces: In TypeScript, I might use
implements MyInterface. In Go, there is noimplementskeyword. If a type has the methods defined in an interface, it implements it. Period. This "duck typing" at compile time is powerful but took a moment to grasp.
3. Generics
I was glad to see Generics (introduced in Go 1.18) covered. In TypeScript, I abuse Generics to create reusable components. Go's implementation is stricter but solves the same problem: writing type-safe, reusable code without casting interface{} (Go's version of any) everywhere.
The Real Challenge: Building a Concurrent Web Crawler
To really test my understanding, I decided to build a simple concurrent web crawler. I wanted to fetch a list of URLs in parallel.
In Python, I would use asyncio or threading (battling the GIL). In Node.js, I'd rely on the event loop and Promise.all. In Go, I use Goroutines.
The Setup
Here is the problem: safely crawling multiple URLs concurrently without visiting the same URL twice. This introduces a classic Race Condition if multiple goroutines try to write to the "visited" map at the same time.
The Solution: sync.Mutex
To handle this, I used sync.Mutex to lock the shared resource (the map) during writes.
1package main2
3import (4 "fmt"5 "sync"6 "time"7)8
9type SafeCounter struct {10 mu sync.Mutex11 v map[string]bool12}13
14// Inc checks if a url has been visited.15// If not, it marks it as visited and returns true.16func (c *SafeCounter) Visit(key string) bool {17 c.mu.Lock()18 // Lock so only one goroutine at a time can access the map c.v.19 defer c.mu.Unlock()20
21 if c.v[key] {22 return false23 }24 c.v[key] = true25 return true26}27
28func Crawl(url string, c *SafeCounter, wg *sync.WaitGroup) {29 defer wg.Done()30
31 if !c.Visit(url) {32 fmt.Printf("Skipping %s, already visited\n", url)33 return34 }35
36 fmt.Printf("Crawling %s\n", url)37 // Simulate network request38 time.Sleep(100 * time.Millisecond)39}40
41func main() {42 c := SafeCounter{v: make(map[string]bool)}43 var wg sync.WaitGroup44
45 urls := []string{46 "http://golang.org/",47 "http://google.com/",48 "http://golang.org/", // Duplicate49 }50
51 for _, url := range urls {52 wg.Add(1)53 go Crawl(url, &c, &wg)54 }55
56 wg.Wait()57}Why this is different
In Node.js/JavaScript, this race condition technically doesn't exist in the same way because user code runs on a single thread. You don't need a mutex for memory access, but you do need to manage asynchronous flow.
In Go, these goroutines are running in parallel on multiple CPU cores. Without c.mu.Lock(), two goroutines could read c.v[key] as false simultaneously, and both would proceed to crawl the URL. The Mutex ensures that the critical section (checking and updating the map) is atomic.
Why Go for my IIoT SaaS?
My upcoming project involves handling real-time data from industrial sensors.
- True Concurrency: Unlike Node's event loop, Go can utilize all CPU cores for heavy computation if needed.
- Performance: Compiled to machine code, fast startup, low memory footprint.
- Simplicity: Easy to read and maintain, which is crucial as the codebase grows.
- Deployment: Single binary deployment. No "dependency hell" on the server.
What's Next?
Now that I have the basics down and have built a thread-safe crawler, I'm starting the implementation of the IoT backend. I'll be using standard libraries where possible and exploring the ecosystem for MQTT and WebSocket support.
Stay tuned for more updates as I build this out!