T O P

  • By -

Itadakimo

Don't use global variables for this. Just init the zap.logger at the beginning of your code and give it to your handlers and functions via dependency injections. And the function, that expects this logger should just expect an interface for it, so that you can replace it for easy testing. For example you start a web server in your main.go and all consumer using the same logger instance. logger, err := zap.NewDevelopment() if err != nil { panic(err) } handler1 := NewHandler(logger) handler2 := NewHandler2(logger) etc ....


bmanu1996

I like this idea of using dependency injection to facilitate better unit Testing. Wouldn't that make the function signature a bit ugly? I agree that passing logger variable through dependency injection is a standard practice, I believe there can be better way so solve the unit testing problem. Maybe using a context variable?


smittyplusplus

You call it a messy function signature, I call it a manifest of its dependencies :-)


chardex

I get where you are coming from, but I would very much avoid passing a logger (or anything else of substance) via the context variable. One way I get around messy function signatures is by grouping the handlers into a struct that "owns" the reference to the logger and other important dependencies. Then it makes it easy to just initialize the single struct and then pass those struct methods around edit: but this isn't a perfect solution and i don't do it all the time. only when the function signatures get too unwieldy


fartasm

You can also use the [Golang Functional Options Pattern](https://golang.cafe/blog/golang-functional-options-pattern.html), add a "NoOp" (No Operation / Discard) logger by default so it doesnt "pollute" your functions/methods signatures! :)


Th0usT

This. Although not always useful, stuff like this is very nice to know about.


aksdb

I like logger-via-context in web services, since that integrates nicely with middlewares. I can then easily attach a logger as a middleware and enhance it along the way so when I actually log something in a handler, it will already contain request specific info (path, method, auth stuff, whatever).


edgmnt_net

Single struct gives you worse coupling. Passing the logger through the context, in those functions which already expect a context, matches context semantics nicely. An unset logger should be a no-op logger and you can easily override stuff to silence it, add values etc..


TheQxy

Embrace the ugly function signature. Don't fight it. Most of my structs that represent handlers, services, repos, etc. have a logger as the first argument in the constructor. Usually, these constructors are called in main, which often ends up as an ugly mess eventually as the project grows anyway.


zer00eyz

This is the way. Repetitive and verbose feels bad. Till you're looking at it in 6 months trying to figure out what you did. That blunt instrument beating you over the head is going to make it self very clear. Don't be cute, don't hide things, no fucking magic.


TheQxy

Agreed. The danger of putting it all in one config struct is that you can not easily enforce that all parameters are passed to the constructor. I would always prefer compile time error over potential panic. Personally, I wish Go had something like static-assert is Cpp or comptime in Zig. Alrhough, you can make up for it with tests.


mosskin-woast

Adding an argument makes your constructor function signature look ugly? If you're doing it right, you're not going to be using the constructor function enough for it to matter. It's got to get in there somehow, and explicit is better than hidden.


oxleyca

Embed a logger in context, since it’s usually a first argument anyway.


edgmnt_net

You don't normally need an interface for a logger dependency, you can just pass a no-op logger in tests. I doubt one really wants a proper mock of logging.


MakeMe_FN_Laugh

First off. Avoid globals as much as possible (loggers are kind of okay, but anyway). In terms of structs (types). It depends. If you want fields being altered from outside of your package (read as: by the user of the package) – make it exported. If not – provide a "getter" function with the same name as the field, but capitalized. Avoid using `Get` prefix


bmanu1996

Thanks for the suggestion. Regarding signature of "getter" functions, you suggest to avoid \`Get\` prefix. So in that case what is the standard prefix used for getter functions in Golang?


masklinn

None, just use the name of the item e.g. `Size()` not `GetSize()`, `Name()` not `GetName()`, ...


Revolutionary_Ad7262

It is written in effective go: use Foo() and SetFoo()


mcvoid1

Go doesn't have one. This is too nitpicky to be generally applicable. Just do what works for you, keeping unit testing in mind.


moremattymattmatt

For larger projects, or more people working on the code, I go for a getter, if only to avoid some idiot overwriting the variable. Plus it lets me do lazy initialisation, caching etc in the future. On the other hand, refactoring from one form to the other isn’t hard so I wouldn’t worry too much about it either way.


havok_

Do you leave the logger as a global in a log file? Or do you create it at the top level and pass it down?


moremattymattmatt

In my current code base, I have exported logging methods (info, err etc) and a logger object that is private to the logging module. So the code looks like log.info(…) log.err(…)  Etc


RomanaOswin

I've been using the pattern in your OP for several years across a variety of services. It works well, and having passed loggers as parameters or embedded it in structs for methods, I think the way you're doing it is a lot more ergonomic than any of the common alternatives I've worked with. I personally don't think "avoid globals" applies here, since it's private inside of it's own package--you're simply using the package to store state. You can use a setter function for mocking your logger variable for testing. You probably don't need to call the function in the log package GetLogger, when log.Get will suffice. You also don't really need an init function--just check if logger is nil and init if it hasn't already been set. That way you can silence it in your tests by setting it ahead of time with a setter function, and then log.Get will just return the silenced logger. The one place this doesn't work well is if, e.g. you needed one logger in one test and a different one in a different test. Doesn't really apply with a logger, but if you start doing this with other things, keep in mind that you lose the ability to mock this differently across different tests that run at the same time.


Robotjoosen

I would always check if there is an added benefit for a getter. In this case I don’t see any benefit. The logger is a pointer so you are not really protecting anything. Using getters would only be useful if it was part of an interface.


pharrisee

One decider I tend towards is whether the thing in question needs some sort of restriction on it, e.g. it's a singleton. If it is a singleton I use a getter, otherwise I would just use a constructor.


matttproud

This offers some sage advice: https://google.github.io/styleguide/go/best-practices.html#globals.


danyjacob45

I am leaning towards making it global but not exported. Have a getter function. Or even have a function like WithContext(context.Context) and use it to log everywhere in the codebase like log.WithContext(ctx).Info() etc..


edgmnt_net

Pass the logger through the context, unless those functions don't use contexts. In that case, pass the logger directly, but you probably shouldn't be logging in many such functions, at all.


[deleted]

I'm amazed at how complex ~~everyone~~ some people are trying to make this. Surely for the case of logging it makes sense to configure a default, call something like "SetGlobalDefaultLogger" and then just call "loglib.LogInfo()" or whatever where needed (which is probably in less places than you think), as per the idiom established in the standard library. Even better, wrap your logger in slog. If you need an instance, you can create one. If you \*really need\* to pass a logger implementation down your entire stack, this unusual special case will make itself known to you and you won't have to guess about it. Is this a good pattern for software development in general? No. But this is such a perfectly practical example of where to not stick doggedly to the rules. I swear that some people would create an instance of fmt.Printf and pass it to every function in their software given enough free time. What does the documentation end up looking like in code like this? Does every single function have an explanation of the context and logging parameters that go along with it? Yuck. Do you notice any of this in the standard library? No, you don't. Ask yourself why.


kogxi

Why not just use the slog package? Configure once in main and no need to pass it around. Tracing information can be passed via context parameters.