Using logger/configs in multi packages best practice for Golang productive

And under internal package I’ve 6 more packages which needs this logger. and each of them is depend on it. (On the log package), is there a nice in go to handle it ?

A good general principle would be to respect the application's choice (whether to log or not) instead of setting policy.

  1. Let Go pkgs in your internal directory be support packages that

    • will only return error in case of problems
    • won't log (console or otherwise)
    • won't panic
  2. Let your application (packages in your cmd directory) decide what the appropriate behavior in case of an error (log / graceful shutdown / recover to 100% integrity)

This would streamline development by having logging at a particular layer only. Note: remember to give the application give enough context to determine action

internal/process/process.go

package process

import (
    "errors"
)

var (
    ErrNotFound = errors.New("Not Found")
    ErrConnFail = errors.New("Connection Failed")
)

// function Process is a dummy function that returns error for certain arguments received
func Process(i int) error {
    switch i {
    case 6:
        return ErrNotFound
    case 7:
        return ErrConnFail
    default:
        return nil
    }
}

cmd/servi/main.go

package main

import (
    "log"

    p "../../internal/process"
)

func main() {
    // sample: generic logging on any failure
    err := p.Process(6)
    if err != nil {
        log.Println("FAIL", err)
    }

    // sample: this application decides how to handle error based on context
    err = p.Process(7)
    if err != nil {
        switch err {
        case p.ErrNotFound:
            log.Println("DOESN'T EXIST. TRY ANOTHER")
        case p.ErrConnFail:
            log.Println("UNABLE TO CONNECT; WILL RETRY LATER")
        }
    }
}

in case I've more things that I need to pass (like logger) what will be the approach/ pattern here

Dependency injection is always a good first choice. Consider others only when the simplest implementation is insufficient.

The code below 'wires' the template and logger packages together using dependency injection and first-class function passing.

internal/logs/logger.go

package logger

import (
    "github.com/sirupsen/logrus"
    "github.com/x-cray/logrus-prefixed-formatter"
    "os"
)

var Logger *logrus.Logger

func NewLogger() *logrus.Logger {

    var level logrus.Level
    level = LogLevel("info")
    logger := &logrus.Logger{
        Out:   os.Stdout,
        Level: level,
        Formatter: &prefixed.TextFormatter{
            DisableColors:   true,
            TimestampFormat: "2009-06-03 11:04:075",
        },
    }
    Logger = logger
    return Logger
}

func LogLevel(lvl string) logrus.Level {
    switch lvl {
    case "info":
        return logrus.InfoLevel
    case "error":
        return logrus.ErrorLevel
    default:
        panic("Not supported")
    }
}

internal/template/template.go

package template

import (
    "fmt"
    "github.com/sirupsen/logrus"
)

type Template struct {
    Name   string
    logger *logrus.Logger
}

// factory function New accepts a logging function and some data
func New(logger *logrus.Logger, data string) *Template {
    return &Template{data, logger}
}

// dummy function DoSomething should do something and log using the given logger
func (t *Template) DoSomething() {
    t.logger.Info(fmt.Sprintf("%s, %s", t.Name, "did something"))
}

cmd/servi2/main.go

package main

import (
    "../../internal/logs"
    "../../internal/template"
)

func main() {
    // wire our template and logger together
    loggingFunc := logger.NewLogger()

    t := template.New(loggingFunc, "Penguin Template")

    // use the stuff we've set up
    t.DoSomething()
}

Hope this helps. Cheers,


There are a few possibilities, each with their own tradeoffs.

  1. Pass in dependencies explicitly
  2. Pass in a context with all dependencies
  3. Use a struct for context to methods
  4. Use a package global and import

All of them have their place in different circumstances, and all have different tradeoffs:

  1. This is very clear, but can get very messy and clutters your functions with lots of dependencies. It makes tests easy to mock if that is your thing.
  2. This is my least favourite option, as it is seductive at first but quickly grows to a god object which mixes lots of unrelated concerns. Avoid.
  3. This can be very useful in many cases, for example many people approach db access this way. Also easy to mock if required. This allows you to set/share dependencies without changing code at point of use - basically invert control in a neater way than passing in explicit parameters.
  4. This has the virtue of clarity and orthogonality. It will require you to add separate setup for say tests vs production, initialising the package to the proper state prior to using it. Some dislike it for that reason.

I prefer the package global approach for something like logging, as long as a very simple signature is used. I don’t tend to test log output, or change logger often. Consider what you really need from logs, and whether it might be best just to use the built in log package, and perhaps try one of these approaches to see which suit you.

Examples in pseudo code for brevity:

// 1. Pass in dependencies explicitly
func MyFunc(log LoggerInterface, param, param)


// 2. Pass in a context with all dependencies

func MyFunc(c *ContextInterface, param, param)

// 3. Use a struct for context to methods

func (controller *MyController) MyFunc(param, param) {
   controller.Logger.Printf("myfunc: doing foo:%s to bar:%s",foo, bar) 
}

// 4. Use a package global and import

package log 

var DefaultLogger PrintfLogger

func Printf(format, args) {DefaultLogger.Printf(format, args)}

// usage: 

import "xxx/log"

log.Printf("myfunc: doing foo:%s to bar:%s",foo, bar) 

I prefer this option 4 for logging and config, and even db access as it is explicit, flexible, and allows easily switching out another logger - just change the imports, no interfaces to change. The calculation on which is best depends on circumstances though - if you were writing a library for widespread use you might prefer to allow setting a logger at the struct level.

I'd usually require setup with an explicit setup on app startup, and always avoid using init functions as they are confusing and difficult to test.