Language design echo chambers
TL;DR: The alias proposal is the wrong solution to a much bigger issue in Go’s ability to meet its stated goals
The alias proposal is a language feature that will probably be added in the next version of Go. If you have time to spare, you can read the design doc on github or some Tylenol to spare you can read the github discussion on the proposal itself. There’s also an interesting mailing list discussion that has more context.
To explain the alias proposal, let’s say you have a customer package and it’s used by a cart package to purchase things.
Later, you decide to move items to their own package so you change the code to move Item.
This works fine if you’re a small codebase, but if you’re Google (the complaint is) then you have a problem because other application’s code doesn’t compile because when they use Client they expect to pass in a customer.Item but your client now accepts an item.Item. They could continue to use your old client, but they won’t get bug fixes either. Under the alias proposal, your new customer package would instead contain an alias.
Now old code still compiles because customer.Item still exists. New code can create item.Item directly, probably with extra features inside the new item.Item, and use the new cart.Client API.
The goal is that you eventually remove the alias definition when everyone has updated customer.Item to item.Item.
Is this a real problem?
Software development isn’t only language syntax. It exists in a greater system of tooling. Indeed, Go’s great tooling support for such a young language has helped its adoption. Pushing a problem to the tools also has the added benefit of not requiring backwards compatibility for a feature that may later be a mistake. It also doesn’t force one company’s needs (the deficiencies of Google’s decision to use a large monolithic repository) upon other companies via a forced language feature.
The specific problems Google has that are creating the alias proposal could be resolved via a tool Google runs to do this mass transformation in a compiler safe way. I don’t think it’s fair to claim Google is too big for tools to work. Still, for the rest of this post we’ll assume this is a real problem that needs a language level solution.
Sidetrack: Google controls go?
Someone once chided me for claiming Google controls Go. No, it’s owned by the Contributors. Go isn’t just the source code, but what it’s released as. From that perspective, Go is whatever https://golang.org says Go is.
< whois golang.org
Registrant Organization: Google Inc.
Google unambiguously owns golang.org and and therefore controls releases of Go.
You said control, not own!
Go’s language definition and syntax is publicly owned by Russ Cox, but that ownership is a git commit from being taken away. It’s ambiguous what would happen if he disagreed with another core team member (I would hope the decision is inaction), but publicly he has final say. Still, it isn’t fair to say Google employees control Go’s language, therefor Google controls Go. They have their own brains!
Instead, it’s how Google writes code that appears to direct the language and new features for Go. That doesn’t have to be a bad thing! They’re full of smart people and tend to write good code, so it’s not so bad to emulate.
The biggest problem with a limited sphere having control is the bubble effect. The problems of users inside this sphere become an echo chamber that magnifies their importance to the people inside it. The culture of the correct way to write code inside this bubble becomes the axioms that the theories of the Go language are based upon.
Example by counter example: the vendor feature
When Go 1.0 was released, people complained it had nothing new. It doesn’t innovate! All of the big things in Go 1.0: light threads, channels, no exceptions, etc. They all existed in previous languages. Innovation isn’t always a good thing: look at fashion.
We can make a great languages just doing the simple parts right. I’m happy not trying new things, and I was happy with Go. And then, Go released the vendor feature… Managing dependencies apparently wasn’t a problem inside the sphere that Google used Go, because when engineers brought up the issue on mailing lists, the typical response was deal with it. This is probably a side effect of:
- When Go 1.0 was released, there was no problem with external dependencies since no on else had Go code.
- Google tends to use monolithic repositories, where dependencies are just code inside the same repository
I imagine we complained enough, because eventually Go released a solution and it was innovative! As far as I can tell, the feature of hijacking absolute import paths depending upon the directory of the source file, allowing two source files to have different ideas of the same absolute import path is the only original feature in Go. It’s no surprise then that I dislike this feature! It can create literally uncallable public APIs, singletons that aren’t actually singletons, and is a half feature that leaves library authors in the dust. It creates surprises in things that code authors assume are intuitive and has multiple gotchas.
Before defending the vendor innovation, think about why other languages didn’t take this path. I see three explanations:
- Go is so wildly different that it only makes sense for Go
- The people who thought of solutions for other languages weren’t clever enough to think of a /vendor folder solution
- The /vendor folder isn’t actually a good idea
My gut feeling on why this feature is so terrible is that it didn’t come from a real need in Google. Bubbles don’t just create features that the bubble thinks is a good idea, they also poorly implement features that the bubble doesn’t fully understand.
This is why I’m honestly excited by the package management team coming up with a solution to package management for Go. After many years, it’s apparent a great solution for package management can’t come from inside Google itself. The generic proverb is necessity is the mother of invention. I imagine the final idea could be added to the go proverb list.
dep’s solution is no one’s favorite, yet dep is everyone’s favorite
If Go followed the same process before the creation of the /vendor folder, and authorized the same people to think of solutions like deprecating go get, we would have a much different looking solution. It wouldn’t innovate at all and instead (just like a well-tailored suit) include working pieces from existing solutions.
The alias proposal, a bubble, and half measures
The alias proposal solves a particular problem Google has migrating definitions. Taken in the context of a segregated monolithic repository, it makes sense as a reasonable solution. My concern is that in the context of the broader Go community it is only a partial measure. I want Go to have a full solution, but when we eventually get it we’ll still have the partial solution floating around.
Part of the bubble is visible in the proposal itself. The proposal talks about removing the alias when the migration to the new package is complete. Unfortunately, this is impossible in an open source project since you can’t know everyone using your code. Even outside the open source community, there are private companies that simultaneously maintain umpteen Go codebases that don’t use monolithic repositories and in these situations it can also be difficult to know when a migration is complete.
The broader problem that’s being ignored is the trouble Go has creating isolated packages. Go’s stated goal includes developing software used by many teams with limited coordination between them. The largest issue working against that goal is inter package dependencies. Inter package dependencies explicitly work against Go’s goal of limited coordination. You can see hints of this in Go’s proverbs.
A little copying is better than a little dependency.
It’s my belief that the alias proposal is a Google specific half measure, when the real problem to tackle is unnecessary inter package dependencies. Go’s implicit interfaces actually resolve part of this problem. For example, let’s say you have two packages.
The problem here is that there’s a package dependency between store and cart which can potentially require coordinating them. It also, honestly, limits Store’s functionality since it should work well not just with a Cart, but with anything that has a Subtotal(). Instead, we can write store as the following.
In this version, store is an isolated package. Because it’s an isolated package, the alias proposal is moot. It can exist as a “cart.Cart”, a “shopping.Cart”, or a “store.Cart” and as long as it implements Totaller everything is fine. Subjectively, I also think this implementation is better. A Store can now render everything that has a Subtotal(), not just carts. Good to note this flows with my general advice for Go accept interfaces, return structs.
We can bring this same philosophy back to the original customer.Client. Is it important that customer.Client.Purchase takes a customer.Item, item.Item, or that it takes a pointer to some structure that has a Price float64 field? We could wrap items with a setter and getter for Price, but practically this is not done for structs (most Go code tends to actively avoid setters and getters).
We run into a similar problem with interfaces. For example, the following code does not work with store2.go
Edit: An earlier version included much different code that did not show my intended point. I apologize and have updated the example.
Here, package app tries to exist in isolation, but cannot expose an interface that store can implement. We could have store directly use app.Totaller, but we create a package dependency that may not be desirable and require coordination when each of them update.
These limitations of Go’s implicit interfaces actually hurt Go’s stated goal by creating unnecessary inter package dependencies, which make it harder to evolve a codebase in parallel. This is the core problem, a problem both the open source and closed source Go community has, and the alias proposal is a subset of this problem.
We can do better
My gut tells me the alias proposal is a half solution to a Google specific problem. I’m confident the same people could create a more general and elegant solution that solves a group of problems the Go community has: a solution I may not be smart enough to come up with myself. This more general solution would simultaneously do more to further Go’s stated goals as well as remove the need for the alias proposal entirely.
I think the real solution is closer to https://github.com/golang/go/issues/8082. It’s a shame the issue was almost immediately labeled as Go 2.0 without consideration. There are backwards compatibility concerns with the specific proposal, but I’m confident those could be resolved with thought. Especially if things like the alias proposal are open to wild ideas like new symbols and syntax.
I would also suggest that the more general solution doesn’t have to cover every corner case of the existing alias proposal. Instead, it only has to cover enough surface area that well designed Go libraries (those that follow best practice API design) are always left with a migration plan.
No half measures
The problem with half measures in a backwards compatible world is that a complete solution is still required. When it is eventually implemented all these half measures are stuck around in backwards compatibility limbo.