Go All the Way: Why Golang is Your Swiss Army Knife for Modern Development
At Oodle's inception, we faced a common dilemma: choosing the right technology stack to get started. With a small team proficient in Go and a big vision, we needed a language that could handle everything from application development to infrastructure management. After careful consideration, we chose Go, and it has proven to be our Swiss Army knife for modern development. Here's why.
The Multi-Stack Mayhem
Picture juggling multiple languages and frameworks across your stack. Many teams live this reality: Python for scripting, JavaScript or TypeScript for frontend, HCL (Hashicorp Configuration Language) for Terraform, YAML for Kubernetes configs, Bash for automation, and traditional languages like Java, C#, or Ruby for backend. Each language comes with its own quirks, dependencies and learning curves. Every addition to the stack means:
- More dependencies to manage
- Additional testing frameworks to learn
- New debugging tools to master
- Extra documentation to maintain
- Longer onboarding time for new developers
While some of this complexity is unavoidable, we can minimize the challenges.
Our Stack
We simplified most of these pain points by choosing Go as our primary language for:
- Application Development: Go
- Infrastructure as Code: Pulumi with Go
- Tooling: Go tools using github.com/spf13/cobra
- Kubernetes: Helm charts wrapped with Go-based tooling
Project Structure
Our project structure now looks something like this:
|- infrastructure
|- pulumi
|- aws
configuration.go
deployments.go
|- kubernetes
|- helm
|- charts
cluster.go
|- src
|- app
|- project
|- collector
|- compactor
|- query
|- util
|- containers
set.go
|- tools
ds_build.go
ds_deploy.go
Why Go Makes Sense
Configuration Sharing Made Simple
YAML and JSON are ubiquitous formats for defining infrastructure and application configurations. While they're human-readable and widely supported, they come with few drawbacks. These formats lack type safety, which makes it easy to introduce subtle errors, such as using strings instead of numbers or mismatching units (e.g., "1000m" vs "1"). JSON doesn't support comments, and neither format supports code reuse or validation at write time. As configurations grow larger, maintaining consistency becomes challenging, and simple typos in indentation or key names can lead to hard-to-debug issues. Furthermore, these formats don't provide any built-in way to handle environment-specific variations or inheritance, often resulting in significant duplication across environments. Go is an excellent choice for defining configurations, except in cases where configurations need to be modified without going through a code build and deploy cycle.
Let's examine how a traditional YAML configuration looks and how we can improve it using Go.
Here's an example in YAML:
deployments:
prod-01:
region: us-east-1
properties:
timeout: 30s
retries: 3
replicas: 3
memory: 2Gi
cpu: 1000m
prod-02:
region: us-west-2
properties:
timeout: 45s
retries: 5
replicas: 5
memory: 4Gi
cpu: 2000m
prod-03:
region: eu-west-1
properties:
timeout: 60s
retries: 4
replicas: 4
memory: 3Gi
cpu: 1500m
And here is how we can improve it with Go:
type DeploymentConfig struct {
Name string
Region Region
Timeout time.Duration
Retries int
Replicas int
Memory string
CPU string
}
// Default resource requirements
const (
defaultMemory = "2Gi"
defaultCPU = "1000m"
)
var ProdConfigs = map[string]DeploymentConfig{
"prod-01": {
Name: "prod-01",
Region: RegionUSEast1,
Timeout: 30 * time.Second,
Retries: 3,
Replicas: 3,
Memory: defaultMemory,
CPU: defaultCPU,
},
"prod-02": {
Name: "prod-02",
Region: RegionUSWest2,
Timeout: 45 * time.Second,
Retries: 5,
Replicas: 5,
Memory: defaultMemory,
CPU: defaultCPU,
},
"prod-03": {
Name: "prod-03",
Region: RegionEUWest1,
Timeout: 60 * time.Second,
Retries: 4,
Replicas: 4,
Memory: defaultMemory,
CPU: defaultCPU,
},
}
Now we can easily use ProdConfigs across our infrastructure, kubernetes and application stack,
ensuring configuration consistency and type safety from development through deployment.
Static Typing: Your First Line of Defense
The debate between static and dynamic typing has no clear winner. While both approaches have their merits, we are in the camp of "Static Typing Where Possible, Dynamic Typing When Needed," as defined in this paper Static Typing Where Possible, Dynamic Typing When Needed:
The End of the Cold War Between Programming Languages. Static typing isn't just about catching errors—it's about building maintainable systems. It helps us catch issues at compile time rather than in production and makes our codebase more navigable and readable through better tooling support and explicit contracts in modern IDEs.
Code Reusability in Action
Consider an example utility that retries an operation a few times before exiting.
package utils
func WithRetry[T any](operation func() (T, error), maxAttempts int) (T, error) {
var result T
var err error
for attempt := 1; attempt <= maxAttempts; attempt++ {
result, err = operation()
if err == nil {
return result, nil
}
time.Sleep(time.Second * time.Duration(attempt))
}
return result, fmt.Errorf("failed after %d attempts: %w", maxAttempts, err)
}
This implementation is now re-used across our infrastructure code, application code, testing utilities, and deployment tools. Otherwise we would have to write this utility in each of the languages used for that respective stack - not to mention managing the dependencies and testing for each language implementation.
Easy to integrate with external commands
While using Go for tooling, invariably, we have a need to execute some of the cli tools like kubectl, helm, docker or other tools. We use Go's native exec package to run external commands.
func RunHelmChart(ctx context.Context, chartName string, releaseName string, namespace string, valuesFile string) error {
cmd := exec.CommandContext(ctx, "helm upgrade --install ", chartName, releaseName, "--namespace", namespace, "--values", valuesFile)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// Usage
err := RunHelmChart(ctx, "my-chart", "my-release", "default", "values.yaml")
We've built similar deployment tooling with Go that handles building Docker images, pushing to ECR, and managing Kubernetes deployments.
Database Schema Management with Goose
Managing database schemas becomes straightforward with Goose, a database migration tool that lets you manage schema changes through SQL or Go functions.
func init() {
goose.AddMigrationContext(upCreateTableUser, downCreateTableUser)
}
func upCreateTableUser(ctx context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
CREATE TABLE user (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);`)
if err != nil {
log.Errorf(ctx, "Error creating user table: %v", err)
return err
}
return nil
}
func downCreateTableUser(ctx context.Context, tx *sql.Tx) error {
_, err := tx.Exec("DROP TABLE IF EXISTS user")
if err != nil {
log.Errorf(ctx, "Error deleting user table: %v", err)
return err
}
return nil
}
And here's how we manage migrations in Go:
package main
import (
"database/sql"
"log"
"github.com/pressly/goose/v3"
_ "github.com/lib/pq"
)
func migrateDB() error {
db, err := sql.Open("postgres", "postgres://user:pass@localhost:5432/mydb")
if err != nil {
return err
}
if err := goose.SetDialect("postgres"); err != nil {
return err
}
if err := goose.Up(db, "migrations"); err != nil {
return err
}
return nil
}
Key benefits of using Goose include:
- Native Go Integration: It runs seamlessly within your Go application
- Version Control: It tracks migration versions and prevents accidental duplicate runs
- Reversible Migrations: It supports both forward (up) and rollback (down) migrations
- Transaction Support: It executes migrations within transactions for data safety
- Flexible Migration Format: It allows writing migrations in either SQL or Go code (we prefer Go)
Package Management That Just Works
Gone are the days of node_modules
hell or Python's virtual environment confusion. Go modules provide clear dependency management:
```go
module github.com/company/project
go 1.21
require (
github.com/aws/aws-sdk-go-v2 v1.24.0
github.com/spf13/cobra v1.8.0
)
In addition, Go's dependency vendoring provides enhanced reliability by allowing you to store third-party packages directly in your project's repository. By committing dependencies alongside your source code in a vendor
directory, you ensure your project remains buildable even if external package repositories become unavailable due to deletion, renaming, or other issues and the ability to completely reproduce builds and analyze the code that went into any past release, even when the original repository is no longer available.
IDE Support That Feels Like Magic
GoLand provides powerful features that make development in Go a breeze:
- Quick navigation with jump-to-definition
- Robust refactoring tools that work reliably
- Built-in debugging that helps you quickly pinpoint and fix issues
- Intelligent code completion that understands your codebase
- AI-powered code suggestions through GitHub Copilot
- Smart code analysis and error detection with Sourcegraph
The AI tooling ecosystem for Go is rapidly evolving, offering developers powerful capabilities.
You can read more about how we use Go to write performant, maintainable and scalable applications in Go faster! and Go Profiling in Production.
Conclusion
Using Go across our entire stack has been transformative. It's not just about using a single language—it's about choosing the right language that's powerful enough to handle everything we throw at it. From infrastructure to application code, Go has proven to be our reliable Swiss Army knife, making our development process more efficient and enjoyable. The consistency in our tooling, the ability to share code across different parts of our stack, and the simplicity of maintaining a single-language ecosystem have allowed our small team to move fast and build with confidence. While some of these benefits may seem trivial on their own, they add up significantly when building and maintaining a large codebase. When you find a language this good, sometimes the best strategy is to "Go all in."
Join Our Team
Are you passionate about building high-performance systems and solving complex problems? We're looking for talented engineers to join our team. Check out our open positions and apply today!