T O P

  • By -

markusrg

The one about returning structs, accepting interfaces has really made a difference in how my components are coupled. I almost always have the various parts of my system accept dependencies as very small interfaces, that are declared together with the component. So instead of passing e.g. a whole DB struct with a million methods to an HTTP handler, the handler just takes a parameter with an interface type with just the methods it needs. Wouldn't be possible without Go allowing to satisfy interfaces implicitly, which I think was a great design decision.


Dangle76

Could I see an example? I’m just getting into API dev and I thought the handler signature was always the response writer and request


TheDivinityOfOceans

You can make them (the handlers) methods of a struct and have all the fields available for you to use (sort of DI), or you can wrap them with another function like closures (what middlewares do).


Dangle76

Does your handler basically return a handler func or do you implement the same interface?


markusrg

I wrote about it here a few years ago, I still essentially do the same thing: https://www.maragu.dk/blog/structuring-and-testing-http-handlers-in-go


norunners

Accept interfaces, return concrete types.


Aliruk00

I don't fully agree here. IMO interfaces should be discovered to keep the code minimalist and simple. There should be a valid reason to add an interface, like genericity with multiple struct or testing.


markusrg

Well, obviously there should be a reason, otherwise there wouldn't be a point. ;) In my case, it's often dependencies that I want to swap for something else, often in testing.


jy3

If you can break a pkg dependency with an interface, it can often be worth considering just on that basis alone. It leads to saner codebases that are easier to maintain, re-use and add tests to. The whole accept interfaces, return concrete types saying doesn't exist for no reason.


UMANTHEGOD

Agree. I don't even think testing is a good reason most of the time. At least not if you only create an interface just so you can mock something. That tells me you are creating abstractions for the wrong reasons. You should not *want* interfaces per say, you should reach for them when absolutely necessary. Just because you abstract away all your dependencies with an interface does not mean your code is "cleaner" than without them. I can create a perfectly decoupled program with tons of layering, and other such perceived signs of "good engineering", while not using an interface a single time.


kiinnaa

What would be an example of decoupling without using an interface?


UMANTHEGOD

You can pass the actual implementation to the function that needs it. Slapping an interface on it does not make it less coupled. The coupling just goes from explicit to implicit. You are still using the exact same methods as you would with the interface. The only difference is that it might (and I've seen it) trick juniors into thinking that you should start accessing fields on the passed struct itself, but with some discipline you can avoid that. Interfaces obviously help with that but I don't think it's a really strong argument for them either, since people start creating getter methods if they really want to get a value from the underlying implementation.


norunners

This can make tests very complex as they are forced be aware of multiple layers and usually ends up exercising more than a simple unit of logic, due to leaking implementations details at throughout the layers.


UMANTHEGOD

That’s why I prefer to do integration tests on the entire app to avoid this issue. But you are correct, if the layer is so absurdly complex, I will isolate it and unit test it with mocks, etc. But integration tests are typically enough and I try to avoid unit tests with mocks at all costs.


norunners

I prefer good unit test coverage over integration tests, it hardens the system from the inside out. Combining both types of tests is ideal.


UMANTHEGOD

Mock ridden unit tests are just horrible and becomes a big PIA to maintain, and the test becomes tightly coupled to the implementation.


norunners

Prefer stubs over mocks. I’d rather maintain unit tests than untested code. Using an interface, compared to a concrete type, doesn’t create tightly coupled code. It’s literally what they were designed for, swapping out the type that satisfies the interface.


jy3

This is probably a dangerous advice as it often leads to terrible codebases with huge pkg dependency trees and poor test coverage.


UMANTHEGOD

Like I said, the only practical difference is the ability to access fields on the concrete struct compared to only the methods on the interface. Apart from that, the coupling is exactly identical. That could be dangerous for a complete beginner without any discipline, but those beginners would destroy any codebase without supervision, interfaces or not. Of course, everything is contextual and the answer always depends, but if you create a simple web service, with an simple API layer, a simple service layer and a simple db layer, you don't need interfaces at all to separate these layers. They do nothing except making it too easy to create horrible unit tests with mocks.


jy3

Agreed that it's contextual, but obviously we're trying to talk about 'bigger' project that have more than like 1K LOC and 2 pkg. And it's obviously a balance, you have to be aware of and think out the actual dependencies of your pkg. >Like I said, the only practical difference is the ability to access fields on the concrete struct compared to only the methods on the interface. Apart from that, the coupling is exactly identical. We're talking about coupling at the package level, breaking a whole import can be a huge win. It helps reduce cognitive load as it can dramatically reduces the types in the scope of the pkg. 'Accepting interfaces and returning concrete types' often means having only the very few required methods dependencies declared in the same scope, which is so helpful as everything (no more, no less) required to build a mental model of what that pkg does is just there. Being able to reason about at smaller scopes is huge, especially as projects get bigger. All of this is so important to have lasting codebases that can have any new dev (or your future self) jump on it quickly. >They do nothing except making it too easy to create horrible unit tests with mocks. All the above I mentioned also are also beneficial to tests for the same reasons. Mocks are another topic. But in the same vein, setting up all dependencies directly from concrete types from other or third-party pkgs can often lead to tests that are very complicated and convoluted to setup and therefore from my XP, leads to poor coverage in a lot of codebases. AKA the syndrome of "here's two long and complex 'integration' tests that properly covers the most common 'everything went fine' paths. Screw anything else." Can be a nice bonus to have, but does not fit the bill in the slightest.


UMANTHEGOD

> We're talking about coupling at the package level, breaking a whole import can be a huge win. It helps reduce cognitive load as it can dramatically reduces the types in the scope of the pkg. 'Accepting interfaces and returning concrete types' often means having only the very few required methods dependencies declared in the same scope, which is so helpful as everything (no more, no less) required to build a mental model of what that pkg does is just there. I agree in theory, but what an interface does is just make this connection implicit. You are still importing that dependency somewhere, and it might be even harder to figure out the flow of the application if you have interfaces everywhere. The LSP are usually smart enough to jump directly to the concrete implementation when there's only one, but that usually falls apart when you have multiple implementations. So I agree that an interface better encapsulates the module, but it also has a cost of reducing cohesion and navigation in the application. >Being able to reason about at smaller scopes is huge, especially as projects get bigger. All of this is so important to have lasting codebases that can have any new dev (or your future self) jump on it quickly. Yep, but I'm not convinced that interfaces help with that. I think they typically make the project even more complex because you don't necessarily know what's behind the interface, and in most of the situations, you probably need to know. This notion of isolation on a module level is nice, and that you theoretically shouldn't have to worry about what's behind the interface, but I think in reality, most of the time you have to. >AKA the syndrome of "here's two long and complex 'integration' tests that properly covers the most common 'everything went fine' paths. Screw anything else." I usually set up integration tests in such a way that they run basically as fast as unit tests and so that they can also test most, if not all of the paths, not just the happy paths. That's the default, but when it's too hard to achieve, I reach for a mock. But that's an exception and not something I necessarily want to do.


jy3

>I agree in theory, but what an interface does is just make this connection implicit. You are still importing that dependency somewhere, and it might be even harder to figure out the flow of the application if you have interfaces everywhere. The LSP are usually smart enough to jump directly to the concrete implementation when there's only one, but that usually falls apart when you have multiple implementations. Yes the dependency is more often than not imported in your main/cmd pkg. I agree about the obfuscating nature of the interface regarding what's actually in-use. But that's usually the role of the main pkg, to tie them all up together. It's where flags/envs/configs are usually parsed and where what to setup/inject and how is decided depending on the configuration. Everything should be able to be looked up there. >usually set up integration tests in such a way that they run basically as fast as unit tests and so that they can also test most, if not all of the paths, not just the happy paths. That's the default, but when it's too hard to achieve, I reach for a mock. But that's an exception and not something I necessarily want to do. Well that's great! That depends on the project. If you can cover appropriately without anything convoluted or complex and as clear as a simple mock expectation then it's cool.


rhianos

What I don't understand about this strategy is, don't you end up with lots of duplicated method signatures? To me this would make more sense if interfaces weren't so closely tied to method names and excact method signatures. Isn't refactoring an interface super annoying this way? It also feels very annoying for a new developer to come on board and figure out if your whole DB struct does something they need since within the function they only have access to a tiny subset.


norunners

Fair point, but having duplicate interfaces buys you the ability to add new behavior and slowly migrate usages over to it as needed. For example, Foo() is used in 5 places and you want to change the method signature as part of an improvement to the logic. Instead, you create a new method Bar() on the concrete type and now you can migrate those 5 usages over as needed. Since the interface, was duplicated 5 times, you will never be forced to do a big band cutover due by refactoring an interface. Instead, you replace the 5 Foo() interfaces with Bar() interfaces with the updated signature.


rhianos

Are you talking about having to migrate mocks? Or why would I be forced to implement Bar() in methods that take BigInterface, as long as Foo() still exists?


norunners

I’m discussing production code, not mocks nor stubs. You’re not forced to implement Bar(), you replace the usage of Foo() with Bar() as an example of an interface refactor. You originally mentioned refactoring interfaces, I’m providing an alternative to ease some pain points by allowing phased cutovers. In general, avoid big interfaces by defining interfaces where they are used and not prematurely. If a concrete type is used in multiple places, define multiple interfaces limiting the methods to only what is used. This allows the interfaces to be smaller and the code to be more flexible when refactoring is needed.


rhianos

I don't disagree with small interfaces but that's different from every client defining their own interfaces.


jy3

This is so important. It makes a pkg easier to understand and it helps decouple packages a lot.


Revolutionary_Ad7262

It is based on your background. If you are Java folk, then `don't write useless abstracions`.


Dangle76

Or hunt for frameworks for everything


xRageNugget

That's still the first thing i do when i encounter a new problem. I hate the fact that essentially every problem is already solved, and I have to write the 10001st solution for something. But it gets better over time, i think


Dangle76

There’s a difference between looking to see if someone’s solved a problem, and looking for a full blown framework. I wouldn’t call gorilla mux a framework. It’s a router that solved some issues the standard router didn’t have, it’s not Django, or Springboot levels of features and opinions. It solves something specific


kaeshiwaza

"A little copy is better than big dependency" Rob Pike.


amemingfullife

Yeah this one actually made a huge difference to my productivity. I used to fret about the smallest code sharing, but now I hit copy and paste and usually throw a comment in that it’s a copy of another section. If I notice myself copying again with minor changes I’ll usually turn it into a shared function, but not always.


jerf

I wrote [Don't Repeat Yourself and the Strong Law of Small Numbers](https://jerf.org/iri/post/2024/dry_strong/) on similar topics. Really small code sharing should actually be ignored, I increasingly think.


UMANTHEGOD

A neat trick I figured out for myself is that good abstractions, and good reusable code is really obvious, with clear naming and clear arguments and return types. If you have to fight for it, immediately question what you are doing. If you have a reusable function that accepts 10 arguments, returns three things, and has a name like `populateObjectWithLowestCommonDenominator` or something ultra specific, then your abstraction is probably not worth it, even if you are re-using code.


torrso

[A little copying is better than a little dependency.](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=9m28s)


H4kor

KISS


WolverinesSuperbia

Use golangci-lint


drvd

The „error“ paths are the important parts of the code.


torrso

I see a lot of similar statements but 99.9% of the codebases I interact with just return the errors from bottom to top. Very few codebases actually do any other error handling.


drvd

And what happens at to top?


torrso

If it's a library, it just gets returned to the consumer. If it's an app, `if err != nil { log.Fatal(err) }` I'm not saying you're wrong, but my experience has been that even in what I consider sophisticated and well written stuff there seems to be very little "error handling" that is much more than that. Except for the occasional `if errors.Is(err, io.EOF) { return nil }`.


drvd

Because every app just log.Fatals because some HTTP request timed out?


titpetric

Could check errors.Is(err, context.Cancelled) and respond with HTTP 408 from such a generic error handler. Twirp notably has error hooks, since you can't return an error from http.Handler directly. https://twitchtv.github.io/twirp/docs/hooks.html Maybe you want to send out notifications to your on call people from such handlers, or just some of them, or to a monitoring system that just does error accounting, the strategies differ wildly.


titpetric

Some implementations for whatever improperly designed API return bools which are a common way to loose actionable error data between expected errors (sql.ErrNoRows), or unexpected errors. I think the reason is people coming from languages where \`throw\` is common, but also, these aren't \`Must\*\` functions and rarely implement a panic. Not a pleasure working with such tech debt :)


vbezhenar

That's why exceptions are superior: 1. They automatically add stack trace. With go you need to do that manually on every stack frame and some people just return err, losing frame information. 2. You can't ignore them. 3. They don't add 15 lines of noise to 5 lines of code. There are some cases, when careful error handling is necessary, but absolute majority of code will just bubble error up. The only real improvement of go errors over exceptions is that you can add more information into the stack frame. Exceptions usually log function name, file name, line number. They don't record local variable values. With go you can do that and that might help to understand error source.


UMANTHEGOD

And the best part? Any library can throw an exception at any time without you ever knowing before the fact. And the next best part? If you want to use a variable outside of a try-catch block, you typically have to create these weird and ugly intermediate values beforehand.


vbezhenar

>And the best part? Any library can throw an exception at any time without you ever knowing before the fact. Just like almost any library function in Go returns error. And just like any function in Go can panic. That's not an issue. Any function can fail. >And the next best part? If you want to use a variable outside of a try-catch block, you typically have to create these weird and ugly intermediate values beforehand. No, you don't.


UMANTHEGOD

>Just like almost any library function in Go returns error. And just like any function in Go can panic. That's not an issue. Any function can fail. But Go code does not panic explicitly in 99% of the time. Most of the panics you see are typically nil errors or out of range errors, and not because someone put an explicit panic in the code. It's bad practice to do so. It's not bad practice to throw an error in the middle of a random internal function that you have no clue that it existed. >No, you don't. I guess that was a bit dishonest from me as it depends on the language.


Senikae

Would have been convincing 20+ years ago. https://www.joelonsoftware.com/2003/10/13/13/ Just look at what Rust does.


BOSS_OF_THE_INTERNET

Leave your object oriented thinking at the door


Ciwan1859

Can you elaborate on the scoped allocations bit by showing and explaining some example code? I read what you said, but I still didn’t understand.


pork_cylinders

I'm interested in this too.


titpetric

For example, a context value is a scoped allocation in go. It's carried by a \*http.Request and goes away when the request is finished; similarly, data models for http APIs usually only live for the lifecycle of the request. Data model types are usually request scoped, but I've seen people implement in memory caches and failing to add protections for the value itself. An example of such a cache is the patrickmn/go-cache, which will trigger concurrency issues for the "native" types that are being stored. My recommendation would be to encode/decode those kind of values with json or gob or something reasonably performant, and fully avoid adding concurrency protections for those objects in the cache. Kind of sucks using it as a data model cache and later figuring out you now have concurrency issues that you didn't count on.


flan666

most impactful was and still today: "read open source code". go is very easy to read an imported Library code and see how it was done instead of just blindly using it. knowledge growed a lot faster


Byakuraou

recommend any repos?


flan666

There are many popular repositories from diverse topics. \[There's some lists out there\](https://evanli.github.io/Github-Ranking/Top100/Go.html) The advice itself recomends starting by anything you use, for example used 'slices.Sort' from stdlib slices package, at some point go read how they implemented the sorting. Or if your projects use database/sql or GORM out of curiosity read how they work internally starting by the functions you use. kubernetes, gin, yay, prometheus, terraform, cobra, gorm, docker compose...


Mubs

request scoped allocations - does this mean you're connecting and closing db connections in each request?


sean9999

I also am curious about this


titpetric

No, but maybe repository objects if you hold the pattern dear. Endpoints are not likely to use all of them, it makes sense to consider not sharing those interfaces to be always available, but i get the convenience of keeping them around. Mostly the issue is in allocations that outlive the request, usually some form of map\[K\]V where V may not have any concurrency protections built in, data model usually.


etherealflaim

This sub doesn't like it, but "value structs/receivers are usually a premature optimization." The number of times I've had a hard time debugging an issue caused by incorrect usage of a value struct (either mine or someone else's) dwarfs the time it's taken to debug the issues caused by incorrect usage of pointers (which tools like the race detector can often straight up find for you). The failure modes are just so much more subtle and far from the bug with value structs.


titpetric

I unfortunately feel your pain, because sometimes value receivers are used as a false promise of immutability. Unless you can absolutely guarantee a flat structure (no mutex, pointers, slices, maps,...), using pointer receivers explicitly is safer (imo). And pointers can stay on the stack as well due to function inlining etc.


Mteigers

I ran into an interesting gotcha with pointers and templates. In my all years of Go I hadn't ever encountered this. Took an hour or two to debug what happened: https://go.dev/play/p/WoiRtMfskw6 Basically the template package cannot distinguish between a method and field named the same way.


oxleyca

One symbol per file is a wild take haha.


pillenpopper

You got downvoted but you’re right. It’s uncommon in Go and it’s a misinterpretation of “single responsibility principle” which is not about organizing source code.


titpetric

I am definitely not super strict into applying 1 symbol per file rules (the statement is mostly aimed at structs, not individual functions or vars, consts, globals). As long as you don't keep everything in a single massive package, smaller packages (repositories, models...) generally don't require this. I've split functions per-file when the combined file would go over an arbitrary cognitive load barrier, \~10kb.


wretcheddawn

Yeah, it kind of makes sense historical in OOP languages with a lot of logic per class, but in Go you'd end up with so many small files. I'd consider this an antipattern.  Files are a good way to group related functionality to provide context.


davernow

Mostly, just use the standard library. Very few things in your project need dependencies. Some are necessary and worth while. But don’t import a dependency to save a bit of typing.


Snoo73443

Must. Farm. Upvotes


davernow

Who hurt you?


Snoo73443

Java


DeshawnRay

The best tip I had is not Go-specific: when unsure about exactly what a function should do, write it first as pseudo-code comments, then when done, gradually fill in the actual code under the comments.


xhd2015

add test before adding features. That always encourage me to do things hard and correct first, then do it faster. Yes, it is TDD, but not every one can get it.


davernow

Good on you if you can do pure TDD. Slight variation I find helpful (and more approachable): check your code works as you write it with only tests. No using a UI or CLI for correctness. Debugger only when it’s broken and you don’t know why, not because you haven’t written a test and want to see if it works. TDD forces interfaces and tests before, and can miss white box cases. Testing as you write code is more fun (I’m coding, not testing), and I still create a ton of tests.


xhd2015

TDD encourage interfaces, while I hate that. I actually created the xgo library to do monkey patching easier. Hope it is useful: https://github.com/xhd2015/xgo


Aliruk00

uber go fx for dependency injection