Defer to the rescue

Defer to the rescue

Table of contents

In case you missed it, I fell in love with Go. In my opinion, it is as perfect as any language can get. The toolings, syntax and community are simply awesome. It is also the language we use at Grace health today.

Yesterday, I was writing a feature that involved adding metric labels to monitor parts of our system. You know, basic stuffπŸ˜‰. Then I came to a code block similar to this.

func (s *DefaultService) DoSomething(ctx context.Context, arg []int64) error {
    var err error
    if len(arg) == 0 {
        return errors.NewInvalidArgumentError("arg is empty")
    }

    a, err = s.DoSomethingElse(ctx, userID)
    if err != nil {
        return err
    }

    err = s.DeleteAll(ctx, userID)
    if err != nil {
        return err
    }

    return nil
}

I needed to add a line of code to help increment a certain Prometheus counter based on the success or failure of the function DoSomething. Something similar to the line shown below.

s.SomethingErrorCounter.WithLabelValues("true").Inc()

If I were to try solving this without a lot of thought (never do that), a plausible solution would be to invoke the counter increment function at every exit(return) point in the function. Something similar to this.

func (s *DefaultService) DoSomething(ctx context.Context, arg []int64) error {
    var err error
    if len(arg) == 0 {
        s.SomethingErrorCounter.WithLabelValues("true").Inc()
        return errors.NewInvalidArgumentError("arg is empty")
    }

    a, err = s.DoSomethingElse(ctx, userID)
    if err != nil {
        s.SomethingErrorCounter.WithLabelValues(strconv.FormatBool(err != nil)).Inc()
        return err
    }

    err = s.DeleteAll(ctx, userID)
    if err != nil {
        s.SomethingErrorCounter.WithLabelValues(strconv.FormatBool(err != nil)).Inc()
        return err
    }

    s.SomethingErrorCounter.WithLabelValues(strconv.FormatBool(false)).Inc()
    return nil
}

Hell no! This gives me the hibbie-jibbies. No one likes code that looks like that. I'm not too fond of code that looks like this. Something needs to be done. Then I recall that Go has a defer statement. Basically, a statement that allows a function or a statement to be executed at the end of the parent function execution. Sweet! Now, instead of that eyesore of a code, I get the same result with the code below.

func (s *DefaultService) DoSomething(ctx context.Context, arg []int64) error {
    var err error
    defer func() {
        s.SomethingErrorCounter.WithLabelValues(strconv.FormatBool(err != nil)).Inc()
    }()

    if len(arg) == 0 {
        return errors.NewInvalidArgumentError("arg is empty")
    }

    a, err = s.DoSomethingElse(ctx, userID)
    if err != nil {
        return err
    }

    err = s.DeleteAll(ctx, userID)
    if err != nil {
        return err
    }

    return nil
}

Clean, simple and straightforward. How can you not love Go 😍.

Note

There is a difference between deferring a statement and deferring a function. When a statement is deferred, the arguments are evaluated at that point but the execution is done at the end of the parent function call. Deferring a function would instead push to function execution to the stack and execute the function at the end of the parent function call. This is why writing my solution as follows won't have worked.

func (s *DefaultService) DoSomething(ctx context.Context, arg []int64) error {
    var err error
    defer s.SomethingErrorCounter.WithLabelValues(strconv.FormatBool(err != nil)).Inc()

    if len(arg) == 0 {
        return errors.NewInvalidArgumentError("arg is empty")
    }
    ...
    ...
}

This would only lead to err being nil and err != nil resulting to false. This isn't what we wanted, so I deferred an anonymous function instead.

So... what is to be gained from this article? Actually, I don't know. I am just excited to share the cool thing that I did. Please go back to fixing bugs and making the internet cool for everyone. Bye πŸ‘‹πŸΎ

Reference