
This blog post is part of a series where I write about my experiences when learning Go. I'll also be sharing my thoughts about some of the differences between Java and Go. In the previous blog post, you can find some resources and real-life projects through which I learned Go. You can also learn about some of the differences in interfaces, functions, and error handling. This blog post will focus on organizing code through packages and directories. In the end, I mention why I believe Java's approach to packages should improve and be more like what Go does.
Packages, directories, and files
Go doesn't require that the directory (physical) structure matches the package (logical) structure as Java does. There's one similarity here, and that is that a single directory can hold only a single package. Let's take a look at this example:
pkg/foo
├── a.go
└── b.go
a.go
and b.go
can belong to the package bar
, but usually, the package name matches the directory name (i.e., foo
).
There are no limitations to the types that a.go
or b.go
can contain (whereas in Java, a public class Foo
has to be declared in Foo.java
)
Imports
A Go package's import path is its module path joined with its sub-directory within the module, for example:
import (
"fmt"
"github.com/example/myservice"
acme_service "github.com/acmeinc/myservice"
)
func main() {
c := myservice.NewClient("http://localhost:8080")
fmt.Println(c.GetInfo())
acme_service.Run()
}
Imports are allowed to have aliases, which is useful when there are packages with the same name.
A notable limitation is that circular dependencies between packages are not allowed (i.e., if foo
imports bar
, directly or indirectly, then bar
is not allowed to import foo
, directly or indirectly).
Can packages be organized into hierarchies?
If you always refer to a type as packageName.typeName
, does this mean you can have only one package in your hierarchy? And how do you cope with complex projects?
Go packages don't have a hierarchy (whereas Java packages do). This may become a problem in complex projects (but when that happens, we should check if the project is too complex and needs to be divided). You can still organize the code in as many directories as your file system allows you. Here's an example from the Conduit project:
pkg/
├── conduit
├── connector
├── foundation
│ ├── cerrors
│ ├── ctxutil
│ ├── grpcutil
│ ├── log
│ └── metrics
│ ├── measure
│ ├── noop
│ └── prometheus
├── http
│ ├── api
│ │ ├── fromproto
│ │ ├── status
│ │ └── toproto
│ └── openapi
│ └── swagger-ui
│ └── api
│ └── v1
├── inspector
├── lifecycle
│ └── stream
├── lifecycle-poc
│ └── funnel
├── orchestrator
Packages and type names
Let's assume that we're working on a driver for a database called FantasticDB
. We'll need the following types: Database
, Client
, Table
, etc.
In Java, you'll end up with something like:
io.fantasticdb.client.Client
, orio.fantasticdb.client.FantasticDBClient
.
Both are pretty common and, at least in my experience, the latter one is more common than the first one.
In Go, we would have fantasticdb.Client
, and that's it. There's no package hierarchy and repeating the package name in a type name (so-called stuttering, which should be avoided in Go code), which leaves us with fantasticdb.Client
.
If I were to change one or the other…
…I'd make Java packages work as they do in Go.:) And here's why.
Let's say we're working on a type that manages user data, which is stored in a fictional FantasticDB database and cached using LightningCache.
The Java code could look like this:
public class UserService {
// option 1
private FantasticDBClient db;
private LightningCacheClient cache;
// option 2
private io.fantasticdb.Client db;
private io.lightningcache.Client cache;
}
// option 1
void setCache(LightningCacheClient cache) {
this.cache = cache;
}
// option 2
void setCache(io.lightningcache.Client cache) {
this.cache = cache;
}
Option 1 reads nicely in the users
package as it's more concise. However, it's not the preferred choice for the authors of FantasticDB and LightningCache. Within a fantasticdb
library and an io.fantasticdb
package, a FantasticDBClient
is too verbose. Option 2 is the opposite. (In practice, we see option 1 a lot.) In many real-world examples, we'll see even longer package names with more nesting.
Once you start thinking about it, the problem is in the package name: it's nested and long.
The Go code would look like this:
// fantasticdb/client.go
package fantasticdb
type Client struct {
}
// lightningcache/client.go
package lightningcache
type Client struct {
}
// users.go
package users
type Users struct {
db fantasticdb.Client
cache lightningcache.Client
}
Why does this code (for the “external” packages and our code alike) look cleaner? It's because of the package name: its declaration (in the fantasticdb
and lightningcache
packages) and its usage (in the users
package). Also, the dot in the variable declaration makes it more readable (fantasticdb.Client
vs FantasticDbClient
)
A win-win strategy?
We have two personas working with code:
- the author of the code
- and the user of the code (a developer writing new code and using existing packages)
Their interests sometimes clash, as we've seen above.
A code author typically prefers simple class names. But those names, like Database
, Client
, or Table
, are often already used by other packages or libraries. In our FantasticDB
example, these types are perfectly clear within the fantasticdb
package. There's no need to name them FantasticDBDatabase
or FantasticDBTable
. Doing so would only bloat the codebase unnecessarily.
A code user, on the other hand, wants names to be specific and meaningful at the point of use. This often leads to “squeezing” extra context into type names, especially when package names are long or nested.
In this context, Go's approach to packages strikes a nice balance: it's a win-win for both the code author and the code user.
Final thoughts
When I started programming in Go, I didn't expect that I'd be spending this much time on packages. Nor did I expect that I'd start with a brief comparison of packages and end up with a whole blog post just about packages.:) It reminded me how even some basic language features can trigger a lot of thinking.