gothinkster / golang-gin-realworld-example-app

Exemplary real world application built with Golang + Gin
https://realworld.io
MIT License
2.52k stars 497 forks source link

Use of global variable to store DB connection #15

Closed kravemir closed 3 years ago

kravemir commented 5 years ago

I'm currently learning go, and I was looking for material about structuring go applications and found this repository. However, it uses global variable to store connection to DB, which I subjectively don't like. So, I was searching this topic on internet,...

I've found article https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html, which is also is against use of globals, because:

The function invokes a package global database/sql.Conn, to make a query against some unspecified database; a package global ... . All of these operations have side effects that are completely invisible from an inspection of the function signature. There is no way for a caller to predict any of these things will happen, except by reading the function and diving to the definition of all of the globals.

By lifting each of the dependencies into the signature as parameters, we allow readers to accurately model the scope and potential behaviors of the function. The caller knows exactly what the function needs to do its work, and can provide them accordingly.

I've found it via reddit discussion https://www.reddit.com/r/golang/comments/9i7ipd/passing_through_layers_vs_global_variables/, where are another examples how to structure application without using global variables.

So, my questions are:

I'm asking this, as I want to learn. I'm biased against global variables in all programming languages (except few special cases for low-level system access). However, I might be persuaded to use global variables in go, if it's strongly encouraged as the "right way" to do things.

PS: thank you for taking your time and sharing the example as public git repo.

camstuart commented 5 years ago

Ok, I'll bite :)

IMHO the use of global variables should indeed be discouraged. But in this case, I think a global database connection is a perfectly valid decision.

Before I explain why perhaps some background on how I came to be here.

I just discovered this awesome concept of "real world apps". I am on the lookout for a new frontend framework that doesn't make me crazy. Looks like elm fits the bill. And being a Go developer, I was pleased to see this repo. As it turns out, I use Gin as well.

And all my Gin projects have a global DB connection. As with all software, there are tradeoffs. I think there are plenty of reasons documented that recommend against the use of global variables, articles that you have pointed out. And I have tried the middleware approach.

I use a global database connection for these reasons;

init()

During the init phase of my apps is when I connect to the database. The global has been declared already, and during init, I connect. Incidentally, this is also where I define all of my routes.

And therefore, I defer the close of the database as the first line in main()

I also favour deployment to serverless cloud platforms, in Docker containers if I can. And one common situation here is the outer architecture keeping my app "warm" for me.

Some serverless implementations recommend connecting to a database within your http handler. Which can result in poor scaling (too many database connections at once)

So connecting in init() is simple, and works well.

func myFunc(db *gorm.DB, .....)

So you might be able to avoid having to pass your database connection into your handlers by using middleware or some other approach, but what about functions that those handlers may call? Again, a single, global variable is easier.

The most common argument I hear about the use of global variables is that in a large codebase they make things messy, complicated, and potentially dangerous. I agree with this view.

But let's remember, you don't go "modifying" the content of this variable. So it could also be considered a constant. So I don't see any risk of it being blatted. And in my 20 years of software, that has never occurred.

And in the microservice world, apps are just not that big anymore.

package somethingOtherThanMain

Globals go out of scope in other packages. But I keep my API/Service code in package main because my philosophy of package is "re-usable" code. Yes, I split my app into multiple files, so I can organise. But not packages. That's an entirely different concept. Never have I written a User package and been able to re-use it in another app. Life is never that simple.

I like a global database connection/handle. It's simple to set up and use, works, and very readable. In my view, this is what I strive for in all the code I write.

While we are on the topic of global variables in a web API / microservice, I also have a global logger.

At the end of the day, I can talk to the database, and log messages anywhere in my code, and so far, nothing bad has ever happened.

kravemir commented 5 years ago

Hello @camstuart,

thank you for the exhaustive answer. Thinking about this awesome concept of "real world apps"...

You've got a point with that life is not that simple to make reusable feature "packages". IMHO, it might be easier to implement it as configurable reusable microservice with API contract (and maybe client library / package to implement connectors).

In the end, in modern development, things tend to get away from monoliths connecting to multiple databases. With YAGNI principle, it is valid point to think about database as a constant for whole application / microservice.

What about testing? Do you do automated tests? If so, how do you deal with having DB connection as global variable?

I'm coming from Java world, and DI helps to create temporary and isolated unit/integration test environment, which is completely isolated from others test. I'm not quite sure, how to accomplish isolation of individual tests, when there's a global variable holding database connection.

wangzitian commented 5 years ago

The function invokes a package global database/sql.Conn, to make a query against some unspecified database; a package global ... . All of these operations have side effects that are completely invisible from an inspection of the function signature. There is no way for a caller to predict any of these things will happen, except by reading the function and diving to the definition of all of the globals.

Global variables are bad behavior, but global utils/middlewares are not bad which could help us reduce code repeating. A better practice might write some utils in global, then managing the database connections in a very small & obvious scope. For example, initializing a database connection might slow, we just initialize once in one place. Or we don't want too much clients connect to the database at the sametime, we write a manager to do the job. Those codes could be share to all apps in your project. BTW, accessing unspecified database(more general, accessing a resource/interface) was hard to prevent in real world even manage database in non global place. My idea of this point is that never trust 3rd resource, we should always check it & log error as early as possible.

By lifting each of the dependencies into the signature as parameters, we allow readers to accurately model the scope and potential behaviors of the function. The caller knows exactly what the function needs to do its work, and can provide them accordingly.

This idea sounds like 'Functional programming', it was good practice in most scenes(easy to write ut, not need to worry break down other function behavior, no state inside function, one input will always have one certainly output), but some time it will lose some flexibility(consensus logic/info might repeated everywhere). Depends on your tradeoffs..

For this small project, I just want to offer a comfortable way to write curd logic for newcomers. A important requirement is that a newcomer should easliy find a place to insert own code, putting similar codes together & making main workflow simple will make it more easier to read. :)

wangzitian commented 5 years ago

@camstuart

But let's remember, you don't go "modifying" the content of this variable. So it could also be considered a constant.

Hit the nail! Offering a bigger context in main workflow will bring convenient for most time in real world.

@kravemir The main side effect of bigger contexts is we might need write more regression testings and it was painful to write unit tests. We could wrapping some small function just do simple things with good unit testings, then regression testings only test main work flow indeed works.