Home

Closures

Created: Dec 2, 2020 5:21 PM

TIL about a helpful usage for closures in go.

I had used closures or anonymous functions before in javascript for callbacks and lua as a way to effectively achieve private functions.

One important aspect of these closures that I found useful today is how variable scope is handled:

// create a function that returns a function with return type int
func foo() func int {
	counter := 0
	return func() int {
		counter += 1
		return counter
	}
}

Here the inner anonymous function is able to access and modify a variable defined in the outer function.

I was working with GRPC server interceptors today and wanted to use a custom logger in the interceptor.

All we have to do to add an interceptor is declare a function that matches the definition for a UnaryServerInterceptor function and pass it as a ServerOption to our GRPC server.

var logger CustomLogger

func serverInterceptor(
	ctx context.Context, 
	req interface{}, 
	info *grpc.UnaryServerInfo, 
	handler grpc.UnaryHandler) (interface{}, error) {
	
	h, err := handler(ctx, req)

	// any custom logic like logging
	logger.Info("Intercepted")
}

func main() {
	// ... 
	server := grpc.NewServer(serverInterceptor)
	// ...
}

In this example, we rely on a package global logger inside the interceptor. I wanted to avoid having a global logger for the package when it would only be used in this interceptor, but I couldn't just pass it into the serverInterceptor function because it wouldn't match the interface anymore.

Enter closures.

Because we can't simply pass an argument to the serverInterceptor function and we want to avoid using a global variable, we can use a closure to wrap the function we care about with another function that we can pass arguments to.

func withServerUnaryInterceptor(logger CustomLogger) grpc.ServerOption {
	return func serverInterceptor(
		ctx context.Context, 
		req interface{}, 
		info *grpc.UnaryServerInfo, 
		handler grpc.UnaryHandler) (interface{}, error) {
	
		h, err := handler(ctx, req)

		// note that this logger is passed in via the outer function
		logger.Info("Intercepted")
	}
}

func main() {
	// ... 
	logger := newCustomLogger()
	server := grpc.NewServer(withServerUnaryInterceptor(logger))
	// ...
}

Here we are using a function that returns another function that matches the interface we care about.

Next time I have an issue of scope I'll consider using a closure as a potential tool in my solution.