
A theory of modern Golang - imwally
http://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html
======
chubot
In other words, use a pure dependency-injection style, and ZERO globals.

I'm writing Python and some C++, and I have been doing this for maybe 5-6
years. It takes a little practice to structure your programs like this, but
once you know how, it has a lot of benefits.

It's not really idiomatic in those communities either. Python and C++
Libraries use globals all the time, although I think it's becoming less
common.

I guess Java programmers have been doing dependency injection for even longer.
It probably became popular more than 10 years ago. But I'm not really sure how
"pure" the Java ecosystem is. Singletons are banned under this style. Instead,
a singleton is just an object you instantiate once in main(), and pass
throughout the rest of your program.

I think the problem with Java is lack of free functions. Static methods
confuse things too.

The pure-dependency injection style is basically _functional programming_ ,
requiring state as an explicit parameter.

~~~
dom0
Well, pure DI also has practical downsides. E.g. anything that wants to log
something has to be able to access the logging configuration — so it needs to
be passed into everything.

~~~
breck
I just pass an "app" object down the tree, and the dependents can do
app.logSomething("foo"). So basically just move all globals in the descendants
to methods on the root app node.

~~~
matthewmacleod
This is also where I always end up with using DI, and it feels like cheating.
Basically pretending that these globals aren't.

~~~
breck
Interesting. How else could DI be done? I'm in the camp that 0 params to a fn
are the best, followed by 1, occasionally 2, and almost never ever 3.

~~~
ubertaco
Ah, yeah, the good ol' fashioned

    
    
        public void doStuff() {
            // ...
        }
    

Everyone loves those!

------
JBReefer
This is basically "Use DI, and move towards SOLID"

Well, yeah! Those are great principles in any language - I find it concerning
(and maybe indicative of something) that the Go community has voted this up.
These are fundamental requirements to building testable, scalable systems in
teams. Having global variables has been bad practice for what, 20 years? One
of the first lines in Javascript, the Good Parts (closest book handy) is:

    
    
      JavaScript is built on some very good ideas and a few very bad ones. ... The bad ideas include a programming >model based on global variables.

~~~
kasey_junk
this article falls into a particular genre "sensible advice that is either so
internalized in other languages or the language itself prevents the pathology
so why would you write that."

It's a winning combo because you can recycle old and simple ideas for a whole
new context.

[edit] felt bad because rereading this it implies the author has bad
intentions. Don't mean that. Just mean it's cute to watch the golang community
learn Java.

~~~
yumaikas
In some ways, I consider Go to be a bit of a redo of the early ideas of Java,
but with a bit more focus on tooling, and a stronger aim towards simplicity.
And Go's standard library authors have the benefit of seeing how Java's early
days played out.

I do enjoy that Go gives a bit more obvious control over memory layout than
Java did, and I enjoy using it as a small projects language. I've never used
it in full anger/production, so I haven't seen the worst of it.

One funny thing about Guice(mentioned elsewhere): I've heard more than one
Googler say that dislike the framework quite a bit, due to how heavily it can
be abused.

One bit of advice that seems to have gone either missing in Java/JVM, or
common knowledge, is the idea that only main/binary packages should have
config options via flags/env vars. Many, many JVM libraries do not follow that
convention, and I've had to hunt down a log4j XML file for Spark in order to
turn down the logging levels before. Would much rather have that be controlled
via a flag.

~~~
chubot
Yes, flags vs. config files in Unix are a classic example of dependency
injection vs. side effects, but at the process level rather than the
programming language level.

At Google pretty much all server configuration is done with flags. Binaries
can and do have thousands of flags. And I view that as a much better solution
than a config file that is searched for.

The idea is that modular units like processes and functions don't "search for
configuration", they just accept parameters, and led the higher level
application configure them. Then the code becomes very straightforward to read
and debug.

------
obstinate
I don't think, in the limit, that this is a scalable approach. While I think
injecting dependencies is fine in some cases, large programs might have
hundreds or thousands of collaborators. It is simply not feasible to
instantiate all of them in the main function then pipe all of them down to the
package that uses them. This is especially true when you keep the interface
small like this program seems to.

I think making the dependency explicit is good when it is likely that it is
going to be desired to change the implementation on a per object basis. If
it's unlikely or meaningless to have multiple different implementations within
the same process, there's nothing wrong with making the configuration global.

Imagine how painful it would be to use the http module if it required the
injection of its clock, compression suite, network interfaces, address
resolvers, loggers, etc. And that's just one module of many.

------
jimrandomh
I think the author's two examples - a logger and a database connection -
should be handled differently from each other. The database connection should
be passed as a parameter, and the logger should be a global variable.

One difference is that it's reasonable to add logging to nearly any function
anywhere, which means that you have to either make the logger global,
preemptively provide a logger parameter to almost everything, or else change a
whole bunch of method signatures at once every time you need to add logging to
somewhere that didn't previously have any log statements. Global variables are
unfortunate, but the other options are much worse.

The second difference is that you pretty much only ever have one logger object
in a process, but processes often use multiple database connections and it may
matter which one is being used. For example, a caller may want to make your
function's database accesses part of its transaction. Or use a unit test
database instead of a real database. Or a number of other things. Omitting the
parameter hides the fact that a function uses the database, which is an
important thing to know about it.

~~~
lsiebert
You may have many different loggers, and different loggers for different parts
of an app. For example, you don't want to log certain database or password
related stuff explicitly as it creates a security issues.

Also it's a lot easier to test logging, or anything that you would us a global
object for, if it's DI based. That said, you can always pass a config object
that holds what would normally be global variables but allows you to override.

------
weberc2
I agree with every bit of this, especially with constructors taking their
dependencies explicitly. In fact, I try to write my Go code in such a way that
constructors aren't needed (sometimes this can't be helped) so the
"constructor" is simply a type initialization expression `foo := Foo{Bar: 7}`.
This keeps everything simple and testable.

~~~
axaxs
Absolutely agreed on the ridding of constructors. Offer the option to set
things (eg the Logger), and if it's nil, use some defaults. This seems much
cleaner than making a huge function signature. The net/http package does this
really well.

~~~
ninkendo
Are you going to do the nil check in every member function though? I run into
this problem with Go a lot, actually. (I'd like to make initialization of a
some member variable optional, but checking if it's nil every time I use it
becomes a pain. Not all types have useful zero values either... I'm looking at
you, map.)

~~~
axaxs
Well, in my cases I usually add an unexported 'check()' method, which is
generally called by every other method that needs state. It would basically
just be something of x.Z == nil {x.Z = DEFAULTZ}

------
weitzj
I favor an DI style as well, but sometimes in rare cases I like using the init
functions.

This boils down to functions which follow the template.Must pattern, i.e.:
either the function can be evaluated or the program should panic.

The other case is read-only lookup tables.

------
transitorykris
I've recently been bit by global state that used function initializers (func
init() isn't the only way to execute logic when a package is imported!). The
package used very reasonable defaults, but panic()'d when things didn't look
right. So, forking, maintaining patches until upstream changes the behavior,
painful.

~~~
kornish
> func init() isn't the only way to execute logic when a package is imported!

What are other ways to execute logic when a package is imported? init() is the
only one I'm aware of.

~~~
transitorykris
package globals

var ( X = blah() )

func blah() int { return 123 }

~~~
codemac
Those are all appended into the Package.init function in the compiled .a, so
they are part of func init() not just in behavior, but in implementation as
well.

------
linkmotif
> "modern Go"

The language was first released in 2007 :). Man...

~~~
chubot
It was developed starting around 2007 but it wasn't released / open-sourced
until late 2009.

------
DAddYE
This isn't "modern go" but more the "standard" in any language.

~~~
weberc2
Sadly not in Python or JavaScript or C++. But yes, definitely good ideas for
any language.

------
marcus_holmes
The stdlib does include some globals/statics, for good reasons (the default
logger, the default http mux).

I'm always wary about advice that says "you must never use {this tool provided
by a language}". Never say never ;)

Globals can be and are useful in some circumstances. Having a blanket ban on
globals because "globals are bad mmkay" is not useful.

I draw this line at "do I need to mock this for testing?". If I need to mock
it to run tests, then it shouldn't be a global and should be DI'd (so I can DI
the mock). If I'm not going to mock it, then it's probably fine to leave it as
a global.

The Db and Logger are perfect examples: I frequently mock the Db connection,
so I can test sql output. I almost never mock the logger, because I only use
it to output meta information. So having a global logger is OK.

YMMV...

~~~
lpghatguy
Shouldn't need a DB connection to create strings. Sometimes want to turn off,
scope, or classify logging especially during testing.

When these new levels of granularity are desired in your project, switching
from globals is no fun.

------
tagrun
The author is basically complaining that Go (and its standard library) isn't
functional.

------
bitwize
An internet discovers dependency injection. Hackernews responds, "Well of
course we knew THAT." The Rust Evangelism Strikeforce is off on a secret
mission in the Balkans.

~~~
MrBuddyCasino
"DI isn't idiomatic in Go" is what I've heard quite a few times. Java did it
so it must be wrong.

~~~
bitwize
Which pretty much confirms that Go is engineered to do things the stupid way
around, and that developers inculcated in its idioms are _just now_ beginning
to see the benefits of what boring old Java programmers have been doing for a
decade or more.

------
mourner
Expected to see an article about the development of modern Go the board game
theory after the advent of AlphaGo and AI generally. Got disappointed. Happens
every time :(

~~~
dang
Ok, have a 'lang'.

~~~
sagichmal
Not cool. That's not the name of the language or the article. Please change it
back.

~~~
dang
It's common enough to have become a convention, especially where ambiguity is
an issue, like in HN titles.

