1.3 What Go Deliberately Leaves Out and Why
Right, let’s talk about what you won’t find in Go. This isn’t a story of neglect; it’s a masterclass in deliberate omission. The designers, Rob Pike, Ken Thompson, and Robert Griesemer, weren’t building a kitchen sink. They were building a very sharp, very specific set of chef’s knives. They looked at decades of language evolution, saw the features that led to endless debates, unreadable code, and 3-hour compile times, and said, “Hard pass.” You’ll either thank them or curse them for these choices, but you can’t say they weren’t intentional.
No Generics… Just Kidding, But It Took a Decade
Let’s get the big one out of the way first. For the first decade of its life, Go shipped without generics. This was the language’s biggest controversy and its most famous missing feature. The reason was blunt: complexity. The designers saw how generics in other languages (looking at you, C++) could lead to baffling error messages and slow compilers. Their initial stance was that you should use interfaces (interface{}, a.k.a. the empty interface) instead.
This was, and I say this with love, a bit of a clown show. You’d write functions that took interface{} and then have to perform a type assertion, which is just a fancy way of saying “please don’t panic at runtime.”
// The Old Way (The Dark Times)
func PrintAnything(thing interface{}) {
str, ok := thing.(string) // Type assertion: "I hope this is a string?"
if !ok {
fmt.Println("Not a string!")
return
}
fmt.Println(str)
}
This was verbose, error-prone, and threw away all type safety at compile time. It was the antithesis of everything else Go stood for. After years of community pleading and very careful design, generics were finally added in Go 1.18. But they did it the Go way: they’re simpler and more constrained than in many other languages. You get the power without the spiraling complexity. The new way is a relief:
// The New Way (A Civilized Era)
func PrintAnything[T any](thing T) {
fmt.Println(thing) // type-safe, no assertion needed
}
The lesson here isn’t that generics are bad; it’s that the Go team would rather ship a feature late than ship a bad feature.
No Exceptions. At All.
Go doesn’t have try/catch/finally. It just doesn’t. This one ruffles feathers. The rationale is that exceptions break the flow of control in a way that makes it difficult to reason about what a function actually does. Will it return a value? Or will it throw an exception and jump five stack frames up to a catch block? Who knows!
Instead, Go uses a painfully simple idea: functions that can fail should return an error as their last return value. You handle it right there, in the line of code where the thing happened. It’s explicit to the point of being tedious, but it forces you to confront errors, not hide from them.
file, err := os.Open("myfile.txt")
if err != nil {
// Handle it. Right now. Don't you dare ignore that error.
log.Fatalf("failed to open file: %v", err)
}
// Only use 'file' if err is nil
defer file.Close()
Yes, you’ll write if err != nil more times than you’ve taken breaths today. You’ll get used to it. The upside is that your code tells a clear, linear story. There are no invisible control-flow grenades waiting to go off.
No Traditional Inheritance
Forget class Foo extends Bar. Go is not an object-oriented language in the traditional sense. It has types and methods, but it doesn’t have type hierarchies. There’s no inheritance. The designers believed that deep, complex inheritance trees make code brittle and hard to understand.
Instead, Go does composition. You build complexity by embedding types within other types. This is has-a relationships instead of is-a. It’s flatter, more explicit, and honestly, it’s a breath of fresh air once you get the hang of it.
type Engine struct {
Horsepower int
}
func (e Engine) Start() {
fmt.Println("Vroom!")
}
// Car HAS an Engine, it isn't an Engine.
type Car struct {
Model string
Engine // Embedded type. This is composition.
}
myCar := Car{
Model: "ThunderCoupe",
Engine: Engine{Horsepower: 120},
}
myCar.Start() // You can call Engine's methods directly on Car.
This promotes reuse without the tight coupling of inheritance. If you come from Java, this will feel weird for about a week, and then you’ll start wondering why everything else is so obsessed with inheritance.
No Implicit Numeric Conversions
This one is a silent guardian against a whole class of subtle bugs. In C, you can freely assign an int to a float or an int32 to an int64 without a second thought. Go refuses to do this. All conversions between numeric types must be explicit.
var myInt int = 32
var myFloat float64 = myInt // COMPILER ERROR. Nope.
var myFloat float64 = float64(myInt) // Correct. You must explicitly cast.
It feels pedantic, but it stops you from accidentally losing precision or having weird type-driven behavior. The compiler is being a good friend here, even if it’s a slightly naggy one.
No While Loops, Ternary Ifs, or Other Syntactic Sugar
Go has exactly one looping construct: for. You can use it as a traditional for loop, a while loop, or an infinite loop. That’s it.
// A C-style for loop
for i := 0; i < 10; i++ { }
// This is your 'while' loop
for x > 5 { }
// This is your infinite loop
for { }
And there is no ternary if operator (condition ? a : b). The Go team believes that readability suffers for the sake of saving one line of code. You just write a full if statement.
The throughline here is a ruthless focus on simplicity, readability, and maintainability at the scale of large software projects and large teams. These aren’t omissions of ability; they are declarations of values. Go is stubborn, opinionated, and sometimes frustratingly plain. But that plainness is its superpower. It means the code you write today, someone else can understand and modify six months from now without needing a PhD in language arcana.