The purpose of DI is to allow the use of a DSL to instantiate and connect objects, with configuration for those objects embedded into the DSL so that the setup and the way things work can be changed quickly without altering code.
Mocking objects for testing purposes and swapping them for the real objects is also something commonly done that is helpful.
There is no "real" DI for Golang as far as I've seen. The only DI I've seen that effectively do the sort of thing I am describing are for Java.
I guess maybe this would be useful for a language like C or old-school C++ where you had to imperatively build your maps and lists and allocate memory and explicitly type your variables and so on, but Go memory allocation and typing are implicit and maps and lists have a nice literal syntax so I don't see any value in a DSL.
As for testing, you can already swap out real objects for test objects--that's a property that interfaces provide; I don't see how a DI framework (e.g, the DSL) helps you here.
I will say that when I've had to operate a Java application, trying to do even the smallest bit of debugging had me digging through all sorts of layers of XML nonsense (Spring, various Spring plugins, and who knows what else) to figure out where the logs were being written. Note that my objection isn't "XML" (versus some other format/DSL), but rather the pointless indirection. I'm sure someone proficient in Java would have no problem, but now your sysadmins need to be Java developers in addition to system administrators (maybe not such a big deal if your shop is already a Java shop and you have people you can ask, but for people operating third party software this is a real pain point). And again, all of that tedium for no apparent value.
The argument for and desire for DI is closely tied to the low code movement. It is tied to configuration as code.
It is certainly a different mentality from straight out coding everything.
I think it is important and will grow bigger in time, because low code is a type of metaprogramming.
You don't "need" to use low code stuffs or do metaprogramming, but if you know how and learn it well you can get much more complex things done quicker than coding everything.
The stuff you are objecting to is due to Spring and such seeking to be a type of generalized metaprogramming instead of focusing on custom DSLs. A custom DSL will have less verbosity, not more.
In a good low-code setup, all your logs should go to the "log" module, and there should be very simple configuration indicating where the logs go.
By DSL...you mean XML/JSON? Because that's literally the only thing I've ever seen used for DI purposes. And then invariably there's still just two versions of any given injectable interface; the one used in production, and the one used in testing.
DI is a decoupling technique. You might only have two interfaces to begin with, but the rough idea is that writing to interfaces and using IoC allows you to make many changes by adding code without having to change old code.
I don't need them in Java or Typescript. The benefit is that I don't have to write these boring, but necessary pieces. As applications grow larger, especially with Go's desire to have an interface with only one method/function, DI requires a lot of boilerplate.
If there was a DI for go that used generate, then I would have compile time checking of dependencies. This would satisfy the community's sense of purity while satisfying my sense of annoyance at having to write this same process for every project.
> The benefit is that I don't have to write these boring, but necessary pieces
The component graph of your application isn't boring, it's the most important part of the thing, and the starting point for anyone trying to build a mental model of the thing. It should be front and center, never hidden away behind generated code.
> The benefit is that I don't have to write these boring, but necessary pieces.
The "boring, but necessary pieces" are usually just a call to a constructor. In my opinion it's almost never worth using a DI system that obfuscates dependency resolution and usually can't be checked at compile time just to avoid that.
Agreed. It seems silly to call "invoking constructors" "boring" but doing all of the same work in XML is somehow more interesting? It seems like you're just adding in a layer of indirection that does nothing besides exchange Go/Java/etc for XML/Groovy/etc at the expense that one must be familiar with the DI framework to understand how the DI files are loaded, linked together, and mapped to your application code.
The main benefit of constructing objects and tying them together via DI is to allow polymorphic handling of responsibilities.
It is more complex than simply "I just make an interface and make everyone agree on that interfere". Everyone agreeing on the interface to use is very unlikely.
The underlying data in different implementations will be different. This is something Golang fails terribly at because it doesn't have polymorphism.
This is, I believe, why there are so few DI systems for Golang, and most of them are of the sort you are referring to as silly.
The way Golang works, and the reccomended patterns for Golang are somewhat anti-DI.
> The main benefit of constructing objects and tying them together via DI is to allow polymorphic handling of responsibilities.
This is already provided by interfaces, as previously discussed. To be clear, dependency injection makes sense; however, dependency injection frameworks don’t make sense to me.
> The underlying data in different implementations will be different. This is something Golang fails terribly at because it doesn't have polymorphism.
Go definitely has always had polymorphism; that’s the whole point of interfaces.
> The way Golang works, and the reccomended patterns for Golang are somewhat anti-DI.
DI (assemble your object graph in main() instead of distributing it across dozens of constructors a la OOP) is idiomatic Go; DI frameworks are not.
Interfaces don't allow polymorphism, because they don't allow you to change the underlying data. The main problem is that you can't ( or at least aren't supposed to ) use any pointers and especially not pointers that point to different data types in different situations.
This sort of behavior is core to polymorphism. It can be done in three ways in Golang ( and probably more too... ):
1. Use serialized messages in channels to do all messages to objects ( disgusting imo... ) It would though at least let one emulate the behavior of message passing / routing languages ( pursuant to original visions of smalltalk etc )
2. Use "unsafe pointers" and just do everything the pay you would in C, deliberately going against the way Golang authors want you to do things.
3. Use reflection and messy if/else in combination with code-generation at compile time. ( this is what a bunch of Golang DI systems do :( )
I don't think you understand polymorphism very well.
I don't give a shit what people are calling DI frameworks these days. I also don't much care for things that simply instantiate a bunch of objects and tie them together. That is only a very elementary variety of metaprogramming.
Essentially, what I am claiming is the Golang is a bad language for metaprogramming, and that in other languages the DI systems they have have become a type of metaprogramming that I think is respectable.
Interfaces absolutely express a type of polymorphism: any concrete type that satisfies the interface can be used in its place. What makes you think otherwise?
> Essentially, what I am claiming is the Golang is a bad language for metaprogramming
That's definitely true, and an explicit choice. Thank goodness!
There is no "either-or" data type in Golang. That's why.
It can only be accomplished by inefficient functional hackery.
In C, you just make a struct, have a type present in the struct, and then cast the struct pointer to extended object types to gain additional functionality.
In this way you can easily accomplish all sorts of fun things like inheritance. Message passing type designs can easily be accomplished also in C.
In Golang? Well... no. You are essentially forbidden from doing any simple casting or extension. You are essentially stuck with hardcoding the crap out of everything or making your own vcall like system build out of Golang types... which you can't really use in the way you want unless you use reflection.
What I can't understand is why anything thinks that Golang does support polymorphism. They admit it themselves. They are working on it. Only the new alpha test versions have a solution for it. The current released version is not polymorphism no matter how much you want to fucking label it that way.
You can't just go "hey it supports a little bit of what everyone knows as polymorphism". That's like saying alcohol is like orange juice because they are both bitter in some cases.
Go lacks parametric ("generics") polymorphism but has interface ("duck typing") and effectively, via the syntactic sugar for embedding, also has subtype ("virtual methods") polymorphism.
It's 30 years too late to complain about three unrelated approaches to dynamic dispatch having the same name.
I think you’re probably trying to make a substantial point but you seem to be mistaken about several things with respect to Go and polymorphism and interfaces such that I can’t figure out what your actual, substantial point is.
> Go is a bad language for metaprogramming
You’re absolutely right here.
> There is no "either-or" data type in Golang. That's why.
Correct here too, Go doesn’t have sum types. If you want sum types, you have to emulate them via interfaces. But I don’t see how that relates since all of this DI stuff seems to be dynamically typed anyway (errors at runtime) assuming you’re not taking a codegen approach anyway.
> In C, you just make a struct, have a type present in the struct, and then cast the struct pointer to extended object types to gain additional functionality.
I don’t understand what you’re trying to do here. First of all, this only works for the first field (and obviously isn’t memory/type safe).
> In Golang? Well... no. You are essentially forbidden from doing any simple casting or extension. You are essentially stuck with hardcoding the crap out of everything or making your own vcall like system build out of Golang types... which you can't really use in the way you want unless you use reflection.
As a general rule of thumb you can do almost anything in Go that you can do in C if only by delving into the unsafe package; however, “unsafe” is almost never necessary—interfaces typically suffice. You certainly can emulate inheritance if you don’t care about type-safety, just like in C. Unfortunately I can’t say more until you clarify your objective.
> What I can't understand is why anything thinks that Golang does support polymorphism. They admit it themselves. They are working on it. Only the new alpha test versions have a solution for it. The current released version is not polymorphism no matter how much you want to fucking label it that way.
I think you must mean some other word because interfaces are the canonical example of polymorphism and Go has the best interfaces in the business. :) I’ve never heard the Go maintainers claim they lack polymorphism (Go does lack type-safe generics and sum types, but so does C). In an earlier post you argued that interfaces weren’t polymorphism because they don’t let you modify the underlying data, which is patently false—this is the whole point of interfaces. In Go:
var r io.Reader // nil
r = someFile // *os.File
r = stringReader // *strings.Reader
Assuming a web application... You can instantiate the object in your main func and then attach it to the server struct so that it is available in every request. When mocking, you can create a mock server that instantiates mock items that implement the interface. If you need something that is contextual, you attach it to the context. In that case, you can still use mock objects, however, you would have to use a different middleware that handles the mock context objects instead of the normal middleware.
By DSL I mean something like how HTML is used to make websites. HTML was derived from SGML, and is related loosely to XML, but does not contain a lot of the complexities of XML.
I do not mean JSON, as JSON is not a good generalized meta-programming notation. People use it that way but it ends up ugly and hard to read.
JSON can be used alright as a configuration notation, but does not represent itself well as a metaprogramming language, since it does not preserve order or allow mixed content. You can get order by using an array of course, but it is much more verbose than the equivalent content in XML or HTML.
You might say, by DSL, I mean "custom markup languages".
I hope to spur conversation on this. I've seen many in the Go community argue against abstractions. Many say, "Pass the database connection in the params." Or "make the DB pool a global variable". How can you write tests for logic without having to instantiate a DB? Many gophers appear to say I should have essentially a giant transaction script for each handler (https://martinfowler.com/eaaCatalog/transactionScript.html). The handler opens the DB, makes it at least function scoped, and all my business logic goes in the handler, or some similar method where the DB connection is passed. Now my tests are functionally integration testings. This makes them both slow, and hard to test for proper error handling.
When I write code in most OOP like languages, I follow the Clean Architecture model. The use case is an actual struct with the interfaces defining the repositories/services as members. I am now free to test the use case in isolation. I can test failure cases to since I return an error, which can be easily mocked. I write a factory to create my use cases. The handlers get the factory passed in as an argument to the constructor for server. I can now test handlers in isolation by passing a factor with mocks.
I aware of making an interface for that. I advocate that. What I see people on r/golang saying, and even in HN, is to just pass the DB connection either explicitly or in the context. I hate this. It makes things bound to DBs.
A "DB connection" in Go is already several layers of abstraction. You could have real production connected to your postgres, integration tests connected to a sqlite file, and unit / functional tests via sqlmock.
Everything is still 'bound to DBs' but that's because your program needs a source of data. Faking a second data source other than the DB via a higher-level shared interface is just inviting integration failures.
Would you have the DB connection be a prominent required parameter of all your functions? Would you have the bulk of your code be integration tests then?
For me this depends on the application. For a REST API I'd probably open a Conn or Tx in an early middleware and carry it on a request context, or via explicit parameter, depending on the HTTP router's features. (In Go that usually means a request context, unfortunately - a more powerful type system would ideally get you some more featureful type-safe routing.) For a more RPC-like or compute-focused API that I might keep some handle to the DB (or other data source) on a method's reciever and route / dispatch to that method. (I think this is the same thing oppositelock suggests elsewhere in this thread - https://news.ycombinator.com/item?id=25807562)
> Would you have the bulk of your code be integration tests then?
I'm not sure if you mean the bulk of my code or just of my tests - for a REST API I would expect mostly functional tests. Do e.g. PUT+GET and make sure the result makes sense. This would be the case regardless of DB architecture.
> Using global state is certainly not idiomatic Go.
Unfortunately it's not this simple (and probably multi-idiomatic).
Go definitely adopts more global state than other languages; I don't know any other language that offers a default-global HTTP client and server. Now, part of that is because Go's stdlib goes out of its way to make these appear stateless even though they are not - and this is good, even if you (often rightly) don't use the default ones.
But I think a lot of people saw those carefully engineered APIs and instead ran with "globals are OK in Go!" Lots of packages have global-level configuration properties - some of this is a hacky replacement for DI e.g. most logger injection. Well, OK, I can support/tolerate some of that because DI in these cases is usually a hacky replacement for real AOP language support. But some of it just shouldn't be global. e.g. Gin debug vs. release vs. test mode should be a setting on the Engine.
And then you get into really bad stuff - I don't know why but it is common to to have a global sql.DB, or sarama.AsyncProducer, or whatnot. A lot of novice Go developers - anecodotally predominately skewed towards previous PHP users, I think because they are not used to have really global variables - use a global for anything concurrency-safe. And this has ended up in a lot of low-quality tutorials/examples/SO questions so I don't see it going away any time soon.
In recent history I have worked a lot with Golang and also encountered the tendency towards globals being the recommended path.
Specifically, I encountered it with logging, frameworks( Gin ), and DBs. It is interesting to me that you called out these three things specifically having encountered them all myself.
I agree also that it is a result of having poor DI support from the language.
I also like your point about PHP users using Golang. The approach Golang uses towards serving web content is very similar to me to how initial PHP frameworks did it.
Go tends to make it easy to setup concurrent things occurring, and so I've found myself spending a fair amount of time speculating on what is thread safe and what isn't.
No matter how much I see it I still do a double take when I see globals in Go modules. Seems like a bad practice to me.
I use "real" in quotes to indicate I am meaning something other than the general meaning of the word "real". What I mean by this is a specific type of DI that I view as effective and a type of meta-programming. Such a DI system does more than just auto-instantiate objects. It also provides a DSL that allows for configuration and ordering of the object instantiation during different phases of system execution.
fx doesn't do that. It is primarily just a convenience way for tying things together in the single intended way. You can provide some configuration but it is not via a DSL; it is by making calls to the fx library.
Realistically to do the sort of DI I am referring to with Golang there would need to be a preprocessing step during compilation that generates Golang code.
I see. Echoing the sibling comment: can you provide a bit more context for the kind of programming that you do? When does such a 'scriptable' DI framework become necessary/preferable? My initial reaction is one of aversion, which makes me think I haven't encountered the kind of problem where scriptable DI solves more problems than it creates.
The closest thing that comes to mind is writing software with some sort of global configuration. i.e.: the user provides some options (CLI flags, environment variables, configuration files, etc.) to change the runtime behavior of the software (e.g. use filesystem storage vs S3 storage, etc.).
In Go, I've always solved this pretty straightforwardly by combining Fx and functional options [1].
Why do you need a DSL. Go already does the things you describe with so little boilerplate that I don’t see what value a DSL could provide to justify its own complexity.
Mocking objects for testing purposes and swapping them for the real objects is also something commonly done that is helpful.
There is no "real" DI for Golang as far as I've seen. The only DI I've seen that effectively do the sort of thing I am describing are for Java.