Hacker News new | ask | show | jobs
by diegs 2670 days ago
About half-way through and I think this is a great article, in particular the quotes and I also agree that the first 4 sections are generally applicable.

One thing I disagree with is the remark about having fewer, big packages. Though conceptually I agree that avoiding having too many public APIs that aren't widely used makes sense, in practice--at least on the types of projects I tend to work on--I find that directing people to split things into a few packages forces them to think about a decoupled design with good APIs between the components. This could certainly be done with discipline inside a single package, but unless everyone working on the codebase is very diligent about this it's easy for abstraction leaks to creep in.

Ultimately it's a judgment call, but I think an earlier paragraph (copied below) is far more important than optimizing on having fewer packages or fewer exported types and functions, especially (as is also pointed out in the doc) you can use `internal` subdirectories to make APIs project-private if you are writing a library that is consumed by other projects, as opposed a service.

> A good Go package should strive to have a low degree of source level coupling such that, as the project grows, changes to one package do not cascade across the code-base. These stop-the-world refactorings place a hard limit on the rate of change in a code base and thus the productivity of the members working in that code-base.

3 comments

I agree with you. I like the way packages are isolated from each other, meaning that to understand a package it is usually by definition a good start to simply read what is there. Smaller packages mean more bite-sized chunks of the program. And I think the discipline of slicing up your program this way is very, very good for the design, and often makes the tests easier to write to by significantly shrinking the surface of what your tests have to "fake" in order to test your code. I think it's just a whole heapin' helpin' o' benefits.

However, I feel myself to be in the minority on this one. To which I basically shrug and write my code with lots of relatively small packages. It really only affects code you're working on, or that your team is working on. Things you pull in as libraries and have no direct interaction with don't matter too much on this front.

If packages are too small it can get hard to understand the code if your not familiar with the structure yet. Working from bottom to top level can work but might also be different because you are missing context for the low level packages to make sense.

In general I found larger packages tend to produce more direct / pragmatic code with less indirection which is usually easier to understand, even though it also feels wrong to me from a theoretical standpoint.

"If packages are too small it can get hard to understand the code if your not familiar with the structure yet."

I solve that with describing the context of the package in the opening prose section. I think this section is underused in every language community I've seen, even though all the automated doc systems support a top-level summary/contextualization/etc.

I think that an advantage of small packages is precisely that it is easier to understand if you're not familiar with the structure yet, by isolating how much structure you have to understand. Large packages, or languages with loose barriers, force you to eat huge swathes of the project at once to understand the code. Small packages are both bite-sized on their own, and also the packages that use the small packages often allow you to gloss over the used package while you're learning that package.

I don't end up with much indirection caused by the package boundaries. (Where there are interfaces I would usually have them anyhow for testing purposes.)

This tends for me to be one of those places where I wonder if I'm just doing something really different than most people. Another example is all the many people over the years who have tried to convince me, with varying level of politeness, that testing code should only use the external interfaces, or dire consequences like having to rewrite all the testing code if I tweak the package will happen. All my testing code uses private interfaces unless there's a really good reason not to, and maybe once in ten years have I had a serious rewrite of the test code come up. The threatened problems don't seem to happen to me. (And I am pretty sure I'd notice them if they did, although I guess I can't completely discount the possibility that I'm just too oblivious somehow.)

Certainly, if they cause you trouble, either because your style is different, or your problem domain is different, or whatever reason, don't use small packages.

I often find myself wishing Go either allowed import cycles between packages, or allowed namespaces within a package. Because they can become unwieldy.

For example, a common convention is to avoid redundancy. Let's say you have a package "builder". Your encouraged to have "builder.New()" as a constructor, not "builder.NewBuilder()". Fine. Now let's say you need two types of builders: One for building "schemas", one for "objects". Your original constructor now needs to be something like "builder.NewSchemaBuilder()" ("NewSchema" would be confusing, since the function creates builders, not schemas). Or maybe turn it into a verb: "builder.BuildSchema()", "builder.BuildObject()". Part of this is due to the lack of statics, or we could've had "builder.Schema.Build()" or something.

You can also split the package up into two packages, one for schema building and one for objects. (Naming here can be tricky, too. Will it be "schemabuilder.New()", or "schemas.NewBuilder()"?) But if the two builders need to share types, you may end up refactoring the common types into a common package that exists only because of the split.

I have a concrete example of this right now for a small query language. The language has data types (common interface Type) and values (Value). Values can tell you their type. Types can construct values. But they're two distinct sets of declarations. The type implementations and the value implementations can't easily be split across separate packages without having a common package that exists only to hold the shared types. It'd be nice to have "types.String" (in types/string.go) and "values.String" (values/string.go), not "lang.StringType" (lang/string_type.go) and "lang.String" (lang/string.go).

Having a namespace option would help here. Everything could be in one package, but under separate namespaces.

Could you work around this somehow using a combination of internal packages and type aliases in public packages?
Not sure. But sounds messy -- type aliases aren't really intended for that.
It was more a sincere question than a suggestion. I haven't written any serious Go since aliases were added and have never had any need for them so far.
Yes, and I think the quoted paragraph has so much more to do with coding around interfaces (behaviour) than with abstraction using non-exported package symbols.