We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

Logger Failure

Here's the code I used to create Linko's logger:

func initializeLogger(logFile string) (*log.Logger, error) {
	if logFile != "" {
		file, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
		if err != nil {
			return nil, fmt.Errorf("failed to open log file: %w", err)
		}
		multiWriter := io.MultiWriter(os.Stderr, file)
		return log.New(multiWriter, "", log.LstdFlags), nil
	}
	return log.New(os.Stderr, "", log.LstdFlags), nil
}

func run(ctx context.Context, httpPort int, dataDir string) int {
	logger, err := initializeLogger(os.Getenv("LINKO_LOG_FILE"))
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to initialize logger: %v\n", err)
		return 1
	}
	// ...
}

Notice that if an error occurs when opening the file, I return an error from initializeLogger, and then in run() I write a message to os.Stderr and return a non-zero exit code. If you used log.Fatal or log.Panic instead, you might have a couple of problems in your code that would:

  • Make it impossible (or very difficult) to test that behavior in a unit test.
  • Prevent the program from running any deferred functions or doing other cleanup.

If you ask me, log.Fatal and log.Panic should be avoided... I don't even like that they're in the standard library, because they couple logging with control flow – but that's a different discussion.

Instead, I prefer to let the caller of the initializeLogger function decide how to behave in the event of a failure! Then, when it's time to handle the error (in the run function), this is one of the few times it's okay to log without a logger (by using fmt.Fprintf) because it was the logger itself that failed to initialize!