Most active commenters

    ←back to thread

    257 points pmig | 15 comments | | HN request time: 0.506s | source | bottom
    1. epolanski ◴[] No.43096608[source]
    Very ignorant about Go, is dependency injection not a thing there?

    E.g. I like declaring interfaces in other languages that model some service (e.g. the database), focus on the API, and then be able to provide different implementations that may use completely different technologies behind them.

    I know that some people don't see the point, but I'll make an example. At my primary client the database layer is abstracted and provided at runtime (or compile time, depends) with the repository pattern. How's that useful? Developers can use a filesystem-based database when working, and don't need to setup docker images that provide databases, anything they do is saved on their machine's filesystem. E2Es can run against an in-memory database, which makes it very simple to parallelise tons of different tests as there are no race conditions. And yes, the saved formats are the same and can be imported in the other databases, which is very useful for debugging (you can dump the prod database data you need and run it against your implementation).

    There's many other situations where this is useful, one of the core products I work on interfaces with different printing systems. Developers don't have printers at home and my clients have different printing networks. Again, abstracting all of it through DI makes for a very sane and scalable experience.

    I don't see how can this be achieved as easily without DI. It's a very nice tool for many use cases.

    replies(6): >>43096619 #>>43096645 #>>43096738 #>>43096799 #>>43096998 #>>43097708 #
    2. tymscar ◴[] No.43096619[source]
    Totally agree with you, but one point against doing what you mentioned is that you want to replicate your prod environment as much as possible in CI, to be able to catch bugs
    replies(1): >>43096663 #
    3. gt0 ◴[] No.43096645[source]
    DI exists in Go, but it's not ubiquitous like it is in the C# or Java worlds.

    Last time I used Go, I used "wire" for DI and was pretty happy with it.

    replies(1): >>43096758 #
    4. epolanski ◴[] No.43096663[source]
    From my experience this has been mostly painless, and yes, in CI you can obviously run against environments using the same stack as prod.

    It may help that I'm writing plain old boring ecommerces or internal tools (scrapers, chatbots, backoffices, warehouse management, delivery systems, etc) and not something more complex and low-level like databases or rendering engines.

    replies(1): >>43098477 #
    5. dgunay ◴[] No.43096738[source]
    DI in its most fundamental form is common in Go via interfaces. People just declare that arguments to their functions must implement some interface, and the caller provides something that satisfies it.

    "DI" in the sense of having a more complex purpose-built DI container is less common. They exist, but I don't see them used a lot.

    If your application code has a ton of dependencies, the direct injection/low magic style favored by Go can get kind of boilerplate-y. I'm not going to comment on which style is better.

    replies(1): >>43101437 #
    6. cflewis ◴[] No.43096758[source]
    I worked on Wire right at the beginning, happy to answer any questions about it.
    replies(2): >>43097112 #>>43097625 #
    7. MrDarcy ◴[] No.43096799[source]
    DI is generally not an explicit thing in Go. As a sibling comment mentioned, there is wire but it simply generates code, it is not a runtime DI framework like Spring has. This is a feature. The code wire generates is the same code you’d normally write, which is what makes it unnecessary for many projects.

    Go has interfaces natively which are implemented in a different way. We never need to say X implements I like we do in other languages. X either satisfies the interface or it doesn’t and the compiler and editor catch this fact early.

    So, you’d simply declare an interface to define the API you described in Go, no need for DI or anything else.

    8. bob1029 ◴[] No.43096998[source]
    DI at its most fundamental interpretation is the act of passing (injecting) arguments (dependencies) to functions.

    Nothing stopping you from doing function-level "injection" of generic or interface type arguments. In this context, function could include the ctor.

    9. badrequest ◴[] No.43097112{3}[source]
    No questions, but a heartfelt thank you. I've used wire for years and plan to continue doing so. :)
    10. gowk ◴[] No.43097625{3}[source]
    Given that the development of Wire seems to have slowed down, do you still recommend using Wire for dependency injection in Go projects? How has your perspective on its design philosophy evolved over time?
    replies(1): >>43098049 #
    11. alexjplant ◴[] No.43097708[source]
    > Very ignorant about Go, is dependency injection not a thing there?

    It is but there isn't a heavy emphasis on a DI framework per se. At its core DI is "inversion of control" insofar as an object constructor receives what it needs to do its job as opposed to the constructor instantiating said requirements itself. In my experience Golang devs don't use of a bunch of meta-programming via tags/annotations/decorators to tell a third-party framework what reflect-y magic it has to do at runtime; they typically just abstract everything behind an interface and pass instances of what you need to a New function that instantiates the consumer object. I personally prefer this to Spring-type frameworks because it's more verbose and checked at compile time. This means one can easily follow what's going on instead of having to dig through a bunch of nested stack traces riddled with Aspect4J esoterica to find out why some magical Maven dependency suddenly broke your entire app for no reason at all.

    Then again the last time I had to bootstrap a complex Spring project was in 2014. Since then I've used Spring Boot for simple 3-tier microservices and it worked fine... it's likely improved since then but I still wouldn't use it for anything greenfield after the loop it threw me for.

    12. cflewis ◴[] No.43098049{4}[source]
    Can’t comment on the slowdown, I left the team a long time ago.

    The goal at the time I thought was very sound: it was a massive PITA that Go programs that ran on cloud servers (of which I would hazard most of them do) were not Write Once Run Anywhere. It was all the same stuff, right? A blob store. A SQL backend. Etc etc. What we wanted was to say “you write a Go program, here run it on GCP. Then Azure. Why not AliBaba if you’re in China”.

    What I am no longer convinced of today is that anyone is really looking for those greased rails because it’s _so much more complicated_ to run on cloud servers than I envisioned. Surely it was going to get more simple? But it didn’t. Networking, authorization, scaling, Kubernetes. The list goes on and on.

    A fully uneducated guess is that Wire does what people who come to it want it to do, but it’s not attracting new users because the use cases are more constrained than envisaged.

    13. prein ◴[] No.43098477{3}[source]
    Interesting idea, but I would worry that developers wouldn’t catch things like slow queries or other performance issues that crop up when using an actual database.
    14. CharlieDigital ◴[] No.43101437[source]
    .NET's DI has constructor injection so I suppose this is similar; you just write your constructor saying "I need an `IDataProvider`" and it can come from DI or direct.

    Question I have for Go though. Without DI, what is the idiomatic way of supplying one of n implementations? An if-else block? Like if I had 5 possible implementations (AzureBlobStore, AwsBlobStore, GcpBlobStore, CFBlobStore, FSBlobStore) and I need to supply 10 dependent services with access to `IBlobStore` what would that code look like?

    In .NET, it would look like:

        // Resolve the concreteImpl in DI
        IBlobStore concreteImpl = switch (config.BlobStoreType) {
            case "Azure": new AzureBlobStore();
            case "Aws": new AwsBlobStore();
            default: new FSBlobStore();
        };
    
        // Register
        services.AddSingleton<IBlobStore>(concreteImpl)
        services.AddScoped<Svc1>();
        services.AddScoped<Svc2>();
    
        // Services just use primary constructor and receive impl
        public class Svc1(IBlobStore store) { }
        public class Svc2(IBlobStore store) { }
    
        // But I can also manually supply (e.g. unit testing)
        var mockBlobStore = new MockBlobStore();
        var svc2 = new Svc2(mockBlobStore);
    replies(1): >>43109679 #
    15. dgunay ◴[] No.43109679{3}[source]
    Yeah it basically is just an if-else or switch block.

        type BlobStore interface { /* methods */ }
    
        // later, when initializing stuff
        var foo BlobStore
        switch {
            case isAzure:
                foo = AzureBlobStore{}
            default:
                foo = FSBlobStore{}
        }
        
        svc1 := NewSvc1(foo)
    
        // Constructors are just functions and are totally 
        // convention/arbitrary. If the fields are public you 
        // can instantiate the struct directly.
        svc2 := Svc2{ BlobStore: foo }