Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

The things you describe are pretty strongly understood as antipatterns in Go.


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.


> Many say, "Pass the database connection in the params." ... How can you write tests for logic without having to instantiate a DB?

You define and use an interface to model the DB, and use that as the mock point. Basic stuff.


To add to your answer for those who aren't familiar with the basics of mocking, here's a StackOverflow answer that I've shared with others--maybe it will help elucidate things: https://stackoverflow.com/questions/19167970/mock-functions-...


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.


Some people say this, but when they do, other more senior people quickly interject and say it's a bad idea.


I agree. I’ve seen more prominent figures argue against global state often. Using global state is certainly not idiomatic Go.


> 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.


> globals in Go modules. Seems like a bad practice to me.

It is, and good packages don't have them.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: