# Go All the Way: Why Golang is Your Swiss Army Knife for Modern Development - **期号**: SRE Weekly Issue #461(2025-01-26) - **作者**: Akhilesh Krishnan — Oodle AI - **链接**: https://blog.oodle.ai/go-all-the-way-why-golang-is-your-swiss-army-knife-for-modern-development/ ## 简介 These folks explain why they use Go for everything: application code, infrastructure as code, tooling, and even as a wrapper around Helm charts for Kubernetes. ## 正文 # Go All the Way: Why Golang is Your Swiss Army Knife for Modern Development ![Go All the Way: Why Golang is Your Swiss Army Knife for Modern Development](https://storage.ghost.io/c/b2/48/b2485d36-0dcd-4e46-b7bd-418ab6e38e18/content/images/size/w2000/2025/01/golangarmyknife2.jpg) 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)](https://developer.hashicorp.com/terraform/language/syntax/configuration?ref=blog.oodle.ai) 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](https://ics.uci.edu/~lopes/teaching/inf212W12/readings/rdl04meijer.pdf?ref=blog.oodle.ai). 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](https://pkg.go.dev/os/exec?ref=blog.oodle.ai) 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](https://github.com/pressly/goose?ref=blog.oodle.ai), 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](https://go.dev/ref/mod?ref=blog.oodle.ai) 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!](https://blog.oodle.ai/high-performance-in-golang/) and [Go Profiling in Production](https://blog.oodle.ai/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](https://jobs.ashbyhq.com/pear/4ecca650-ccbe-45eb-911d-211e877812ca?ref=blog.oodle.ai) and apply today!