Why Go is the logical next step for Python developers
As a professional Python developer for the last 6 years and hobbyist for the last 10 years, I have come to appreciate this language as my daily driver. But after such a long time using it, I needed to challenge myself by learning a new one since Python’s weaknesses were becoming evident for certain use cases.
In the past 5 years, I have learned many different languages: Dart for Flutter, JavaScript for React and Vue.js and even did some C++ for Arduino projects. But none of them really made me a better programmer nor truly complemented my toolbox and I disliked most of them, especially you JavaScript.
So my goal was to find a more performant language for CLI tools, system utilities, and heavy-duty tasks. It should also have static typing built-in and excel where Python struggles, but most importantly I wanted a language that I would enjoy using as my next go-to for scripting and personal projects.
But first, let’s discuss my experience with Python’s limitations.
Python strengths and weaknesses
When it comes to developer experience and syntactic sugar, Python is the king. It doesn’t take long to appreciate the power of string interpolation, f-strings, dict and list comprehensions, tuples, and powerful unpacking. Python also has one of the best standard libraries out there. You can accomplish so much without importing a single package, whereas JavaScript needs a package for the simplest tasks.
The packaging ecosystem is mature and powerful. If you want to process large batches of CSV files, Pandas is there for you and can dramatically reduce processing time, sometimes by half. Want to create a simple API? FastAPI offers both fast performance and rapid development. If you want something more opinionated and batteries-included, Django will serve you well like it has for me the past 6 years.
But with these strengths come some drawbacks that become glaring after 5+ years of use.
First, Python’s type system is optional and not enforced at runtime. Sure, you can use mypy, Django stubs, and type annotations, but it becomes cumbersome on larger projects, especially if you didn’t use types from the beginning. Between the different Python versions, Django-specific typing, and the overhead of proper type annotations, I often feel like I’m fighting against the language rather than working with it.
I have been working on a large codebase that was slowly adopting type annotations in Django and it was a miserable experience, especially with strict type checking.
Second, while Python is simple to write for most tasks, concurrency and asynchronous programming feel unnecessarily complex. The GIL (Global Interpreter Lock) limits true parallelism, and choosing between threading, multiprocessing, and asyncio adds cognitive overhead. As such I’ve never felt comfortable with Python’s most advanced features.
Third, Python offers too many ways to accomplish the same task. While flexibility can be good, it often leads to inconsistent codebases. The tooling ecosystem is similarly fragmented: there are dozens of ways to manage virtual environments (venv, virtualenv, pipenv, poetry, conda), multiple linting tools (pylint, flake8, black, ruff), and various packaging solutions. This fragmentation makes Python projects harder to standardize across teams. Worst of all is that people often try to be very clever with Python, using metaclasses, decorators, list comprehensions, and other advanced features that can make code hard to read and maintain when really, the simplest solution is often the best.
I have been there and when I read some of my old code, I cringe a bit…
Finally, distributing Python applications is surprisingly difficult. Users need the correct Python version installed, must manage dependencies, handle virtual environments, and deal with potential conflicts. Creating standalone executables requires third-party tools like PyInstaller, which often produce large binaries with their own quirks.
In my pentesting period most of the tools were built using Python, while the tools were fast enough and well built, the installation could get tricky.
So I decided to explore alternatives and see what else was out there.
Building with Go
My choice came down to Rust or Go. Rust is consistently praised as the most loved language in developer surveys, but after careful consideration, I realized it might not be the right fit. While Rust offers exceptional performance and memory safety, its steep learning curve and complex ownership system seemed like overkill for my use cases, again the goal was to have a language that I like using. The job market for Rust is also relatively small compared to Go, especially in France where I live.
Finally, Go emerged as the perfect candidate for several reasons:
Minimal syntax, maximum clarity
Go’s philosophy of simplicity resonates with the Zen of Python. However, Go takes this even further by enforcing it at the language level and it is not up to you. With only 25 keywords, Go reduces the necessity of thinking about clever tricks that harm readability and add unnecessary overhead, it is refreshing and surprisingly easy to adopt.
Compiled language with excellent performance
Go compiles to native binaries, resulting in fast execution and easy deployment. Many tools you probably use daily are written in Go: Docker, Kubernetes, Terraform, GitHub CLI, and Hugo (which powers this blog). The compilation is so fast it feels like nothing happened. My entire blog rebuilds in milliseconds.
While “blazing fast” is relative (Rust, C++, Java and Zig can be faster), Go hits a sweet spot between performance and developer productivity whereas Rust often sacrifices developer experience.
Concurrency as a first-class citizen
Go’s concurrency model is refreshingly simple. Unlike Python’s multiple approaches (threading, multiprocessing, asyncio, queue), Go provides goroutines and channels. No function coloring, no async/await complexity. Just add go
before a function call:
// That's it - this runs concurrently
go processData(data)
// Channels make communication simple
results := make(chan Result)
go worker(tasks, results)
Unified, opinionated tooling
This might be Go’s killer feature for me. The language includes:
go fmt
for one way to format codego test
for built-in testing frameworkgo mod
for dependency management made simplego build
for compilation and cross-compilation
We often as developers forget how important the developer experience is, and Go nails that one perfectly.
Type safety from day one
Go’s type system is mandatory and fundamental to the language. The compiler catches errors at build time, not runtime. While this means more upfront work, it pays dividends in reliability:
// The compiler ensures this is always called correctly
func ProcessUser(id int64, name string) (*User, error) {
// Implementation
}
Your IDE provides intelligent autocomplete, refactoring is safer, and large codebases become more maintainable.
Deployment is just copying a file
go build
produces a single, statically-linked binary. No Python installation required, no virtualenv, no pip install. Just copy the binary and run it. Cross-compilation is trivial:
GOOS=linux GOARCH=amd64 go build
GOOS=windows GOARCH=amd64 go build
GOOS=darwin GOARCH=arm64 go build
…but it’s not all sunshine and rainbows
Error handling verbosity
Go lacks exceptions. Every error must be handled explicitly:
file, err := os.Open("config.json")
if err != nil {
return nil, fmt.Errorf("failed to open config: %w", err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
This verbosity is controversial. It forces you to consider failure cases. However, it can make code repetitive and harder to follow for complex flows, and I feel like it is a step back from Python’s exception handling.
Different programming paradigm
Go doesn’t support traditional OOP inheritance. Instead, it uses composition and interfaces:
// No class hierarchies
// No method overriding
// No super() calls
// Instead, you compose behaviors
type Logger interface {
Log(message string)
}
type FileWriter interface {
WriteToFile(filename string, data []byte) error
}
// Embed interfaces to compose functionality
type FileLogger struct {
Logger
FileWriter
}
For Python developers used to class hierarchies, this requires rewiring your brain to think in terms of behaviors and composition rather than inheritance, I am still not sure where I stand with this one.
Less “batteries included” than Python
While Go’s standard library is good, it’s not as comprehensive as Python’s. You’ll need third-party packages for things Python includes by default:
- No data science libraries comparable to NumPy/Pandas
- Limited built-in data structures (no OrderedDict, defaultdict, etc.)
- AI and machine learning libraries are still maturing but not as rich as Python’s ecosystem (TensorFlow, PyTorch, etc.)
- Less syntactic sugar and built-in features
Real-world example: Building Synaudit
To test Go’s suitability, I built Synaudit, a CLI security auditing tool for Synology NAS systems. This project highlighted Go’s strengths:
- Performance: Port scanning that would take 10-15 seconds in Python completes in under 2 seconds thanks to the easy-to-use and implement concurrency model and compiled nature of Go.
- Deployment: Users just download and run a single binary with no installation process. You can run it on your NAS directly without Python dependency nightmares, important since this tool is not only for pro users.
- Concurrency: Scanning 50+ ports simultaneously was trivial with goroutines.
- Reliability: Type safety caught numerous bugs at compile time, although I had to get used to the explicit error handling and structured error types.
It was however far more challenging than I expected. What took me a week to build in Go could have been done in a day with Python.
Real-World Example: Why I Built Synaudit in Go
To truly test Go’s suitability for my use cases, I decided to build Synaudit, a CLI security auditing tool for Synology NAS systems. This project was the perfect example of why Go was the right choice for this type of application.
The Problem: Synology’s Security Audit Challenge
While I love my Synology NAS, the DSM interface can be painfully slow and cumbersome for quick security audits. I found myself constantly jumping between multiple services and applications just to get a comprehensive understanding of my system’s health and security status.
Although Synology provides some built-in tools, none offer the speed or comprehensive bundling of all the security checks I needed. More importantly, there was no easy way to:
- Quickly scan for common security misconfigurations
- Get a consolidated report of all security issues
- Run audits programmatically or integrate them into automated workflows
- Share a simple tool with the community without complex installation procedures
Why Go Was Perfect for Synaudit
Building Synaudit highlighted Go’s strengths in ways that convinced me of its value:
1. Performance Where It Matters Most Network operations and concurrent API calls are at the heart of Synaudit. The tool needs to:
- Make dozens of simultaneous API calls to different Synology endpoints
- Perform concurrent port scans across multiple services
- Process and correlate data from various security checks
Port scanning that would take 10-15 seconds in Python completes in under 2 seconds thanks to Go’s goroutines. Here’s the actual implementation:
func checkPort(host string, portInfo PortInfo, ch chan<- PortStatus, wg *sync.WaitGroup) {
defer wg.Done()
address := net.JoinHostPort(host, fmt.Sprintf("%d", portInfo.Port))
conn, err := net.DialTimeout(portInfo.Protocol, address, 900*time.Millisecond)
// Process result and send to channel
ch <- portStatus
}
// Scanning 50+ ports concurrently is trivial
for _, port := range ports {
wg.Add(1)
go checkPort(host, port, ch, &wg)
}
2. Zero-Dependency Deployment This was crucial for a security tool. Users can:
- Download a single binary and run it immediately on their NAS or local machine
- No need to install Python, manage virtual environments, or deal with dependency conflicts
3. Structured Error Handling for Security Tools Security tools need reliable error handling. Go’s explicit error handling, while verbose, ensures that every failure scenario is considered:
func fetchSynologyData(url string) (*SynologyResponse, error) {
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("HTTP request failed: %w", err)
}
defer resp.Body.Close()
// Every error path is explicit and handled
}
4. API Integration Made Simple Synaudit makes compound API requests to Synology’s undocumented APIs. Go’s strong enforced typing and JSON marshalling made working with complex API responses straightforward:
type SynologyResponse struct {
Success bool `json:"success"`
Data SynologyResponseData `json:"data"`
}
type UserListData struct {
Total int `json:"total"`
Users []struct {
Expired UserStatus `json:"expired"`
Name string `json:"name"`
} `json:"users"`
}
The Development Experience
Building Synaudit was both challenging and rewarding. What would have taken me a week in Python took about a week in Go.
The type system caught numerous bugs at compile time that would have been runtime errors in Python. The explicit error handling forced me to think about edge cases I might have missed. The concurrent programming model made complex operations feel natural rather than bolted-on.
Most importantly, the deployment story is exceptional. Users don’t need to worry about Python versions, virtual environments, or dependency installation. They just download and run.
Why Go makes you a better programmer
Learning Go improved my programming in several ways:
- Explicit error handling made me think about failure cases in all my code, even Python. I really like the fail-fast approach.
- Composition over inheritance led to more modular, testable designs.
- Static typing discipline carried over to my Python code, making me use type hints more consistently, but still not as much as I should.
- Simplicity constraints taught me to avoid clever tricks in favor of readable code.
When to use which language
After this exploration, I’ve developed clear guidelines:
Use Python for:
- Data science and analysis
- Web applications (Django/FastAPI/Flask)
- Rapid and dirty prototyping
- Scripting and automation
Use Go for:
- CLI tools and system utilities
- Network services and APIs
- DevOps tooling
- Performance-critical applications
- Anything you need to distribute as a binary
- Hosted scripting and automation tools
Conclusion
To be clear, Go isn’t replacing Python in my toolkit. It’s complementing it. Python remains unmatched for rapid development of web applications and dirty scripting tasks and I still love using it.
If you’re a Python developer feeling limited by performance, deployment complexity, or type safety, give Go a week of your time. You might find, as I did, that it’s the perfect complement to your existing toolkit.
Resources
Here are some resources to help you get started with Go that I found useful: