Go Variables: Why the Compiler Is Your Strictest (and Best) Colleague
Variables, types, and why Go is stricter than your code reviewer
The Moment Go Rejects Your Code
You’ve written Go for about ten minutes when it happens. You declare a variable, decide you don’t need it, leave it there. The compiler stops you cold: x declared and not used. In Python, JavaScript, even Bash, that variable would just sit there quietly. Nobody would care.
Go cares.
Your first instinct is annoyance. Your second instinct, about a week later, is gratitude. Go’s compiler behaves like that senior engineer on your team who won’t let anything sloppy through code review. Not because they enjoy blocking your PR, but because they’ve seen what happens when you don’t catch small things early. A unused variable today is a misread config value at 2am next Thursday.
That strictness is a feature. Let’s understand how Go’s variable and type system works, because it’s different enough from most languages that it’s worth learning properly.
Hi — this is Pushpit from CloudOdyssey . I write about Cloud, DevOps, Systems Design deep dives and community update around it. If you have not subscribed yet, you can subscribe here.
Two Ways to Declare Variables
Go gives you two ways to declare a variable. They’re not interchangeable, and knowing when to use each one matters.
The long form uses var:
var endpoint string = "https://api.example.com"
var timeout int = 30
var debug bool // no value — gets the zero value (false)The short form uses :=:
endpoint := "https://api.example.com"
timeout := 30Both work. The rules about when to use each one:
:=only works inside a function. You cannot use it at the package level.varworks anywhere inside functions, outside functions, at the top of a file.When you want to declare a variable but not give it a value yet, use
var.
Most of the Go code you write day-to-day will use := inside functions. You’ll use var when you’re declaring something at the package level or when the zero value behavior is exactly what you want.
Zero values are not null. This is important. Every type in Go has a zero value the value it holds when you don’t initialise it.
var port int // zero value: 0
var host string // zero value: "" (empty string)
var active bool // zero value: false
var ratio float64 // zero value: 0.0Nothing is nil unless it’s a pointer, slice, map, channel, or function. For basic types, you always have a usable value. No nil pointer exceptions hiding in your config parser waiting to ruin your day.
Go’s Type System
If you’ve written Terraform, you know it has variable types: string, number, bool. Go’s type system is similar in intent it’s explicit, it’s enforced, and it prevents you from accidentally treating a port number like a hostname.
Here’s a practical breakdown of the types you’ll actually use:
A few practical notes worth burning in:
int vs int64: On 64-bit systems (everything you deploy to), int is 64 bits anyway. The distinction matters when you’re explicitly working with a protocol or binary format that specifies a field size like reading a Kubernetes API response that returns an int64 timestamp. The rest of the time, int is fine.
float64 for almost everything: Don’t use float32 unless you have a specific reason. The precision difference matters more than the memory savings in most operational code.
Strings are immutable. You can’t change a character in a string after it’s created. You can create new strings from old ones, but the original doesn’t move. This makes strings safe to pass around concurrently, which matters in the goroutine-heavy code you’ll write later in this series.
No truthy/falsy. if someInt {} is a compile error. You must be explicit: if someInt > 0 {}. No guessing whether zero counts as false.
For 95% of your Go code, you’ll reach for int, float64, string, and bool. The specialised types exist for systems code, protocol implementation, and performance-critical paths.
Constants
When a value should never change an HTTP status code, a log level, a default timeout use const.
const DefaultTimeout = 30 // untyped, infers type from context
const MaxRetries int = 5 // explicitly typed
const APIVersion string = "v1" // string constant
Constants are evaluated at compile time. You cannot assign a variable’s value to a constant, and nothing can change a constant at runtime. The compiler enforces this completely.
Go also has iota for incrementing integer constants, which is cleaner than manually assigning values to enumerations:
const (
LogDebug = iota // 0
LogInfo // 1
LogWarn // 2
LogError // 3
)This shows up everywhere in real Go tooling log levels, status codes, state machine states. Once you see it, you recognise it quickly.
Type Conversions Are Always Explicit
Go has no implicit type conversion. If you have an int and a function expects a float64, you convert it manually. No exceptions.
timeout := 30 // int
ratio := float64(timeout) / 100 // explicit conversion to float64 before dividingThis feels verbose until you’ve debugged a Python script where a config value came in as a string "30" and got silently compared against an integer 30, returning false. Go would have caught that at compile time. Every type conversion is visible in the code, which means every potential mismatch is visible too.
Think of it like Terraform’s type system you can’t pass a number to a variable expecting a string and expect it to just work. You have to be explicit. The friction is the feature.
Hands-On: A Cloud Config Struct
Let’s put this together into something real. This is the kind of code you’d write at the start of a cloud service client:
package main
import "fmt"
func main() {
// Declare config using short declaration
endpoint := "https://api.internal.example.com"
port := 8443
retries := 3
debug := false // not set in this environment — defaults to false
// If debug mode is on, you'd log verbosely
// Checking explicitly — no truthy/falsy ambiguity
if debug {
fmt.Println("Debug mode enabled")
}
// Build a connection string using Sprintf
// Note the explicit int-to-string handling — Printf handles this via verbs
connString := fmt.Sprintf("%s:%d (retries: %d)", endpoint, port, retries)
fmt.Println("Connecting to:", connString)
}Output:
Connecting to: https://api.internal.example.com:8443 (retries: 3)Nothing fancy, but notice: debug was never set to true, so the block doesn’t execute. Zero value for bool is false, which is exactly what you want for an optional debug flag.
In the Cloud Wild
A real incident pattern in ops tooling: a Python-based config parser reads a YAML file and loads timeout: 30. YAML parses this as an integer. Elsewhere in the codebase, a function that builds a connection URL does string concatenation using +. The integer 30 hits that function, Python throws a TypeError at runtime, in production, under load.
That bug cannot happen in Go. If timeout is declared as int, you cannot concatenate it with a string. The compiler refuses to compile the code in the first place. The error surfaces in your IDE or CI pipeline, not in your on-call rotation.
Type safety at compile time isn’t pedantry. It’s production reliability.
Common Mistakes
:=outside a function. Package-level declarations must usevar. Using:=at the top level is a compile error.Declaring a variable and never using it. The compiler will not build your binary. This is intentional.
Confusing
=and:=.=assigns to an existing variable.:=declares and assigns. Using=before declaring a variable is a compile error.Variable shadowing in inner blocks. If you declare a new variable with
:=inside anifblock using the same name as an outer variable, they are two different variables. The outer one is unchanged. This trips everyone up at least once we’ll cover it properly in article 4.
What’s Next
You can now declare variables, understand types, and write constant enumerations. Next up: composite types. Arrays, slices, maps, and structs. The data structures you’ll use to model a Kubernetes namespace, a list of pod names, or a config map. This is where Go starts to feel genuinely useful for the work you actually do.





