Building a web api with Go coroutines

Introduction

On this page I described building a simple web API with a Postgres database at the backend. For this I used the Gin web framework which is pretty fast in itself. A nice extension however would be to use coroutines and make the API truly asynchronous.

Well, it took some time, but I managed to do it, and here is how:

The OperationResult struct

To streamline the handling of errors, I have built a simple OperationResult struct:

package models

type OperationResult[T any] struct {
	Value  *T
	Result error
}

func ConstructWithoutError[T any](value *T) OperationResult[T] {
	return OperationResult[T]{
		Result: nil,
		Value:  value,
	}
}

func ConstructWithError[T any](errorResult error) OperationResult[T] {
	return OperationResult[T]{
		Result: errorResult,
		Value:  nil,
	}
}

A short description:

  • This is a generic struct, that can take any type
  • There are two fields: the Value which the operation returns and Result, which is an error type
  • The two methods are just utilitymethods to construct either an error struct or a non-error struct

The ListEvents handler

Let us look at a simple example of errorhandling and coroutines, the ListEvents handler. All this handler does is list the available events in the database. It looks like this:

func ListEvents(c *gin.Context) {
	result := make(chan models.OperationResult[[]models.WebEvent])

	dbConnection, err := db.GetConnection()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err})
		return
	}
	if dbConnection == nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "No database connection"})
		return
	}

	go func(context *gin.Context) {
		var webEvents []models.WebEvent

		dbResult := dbConnection.Connection.Find(&webEvents)
		if dbResult.Error != nil {
			result <- models.ConstructWithError[[]models.WebEvent](dbResult.Error)
		} else {
			result <- models.ConstructWithoutError[[]models.WebEvent](&webEvents)
		}

	}(c.Copy())
	finalResult := <-result
	if finalResult.Result != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": finalResult.Result})
	} else {
		c.JSON(http.StatusOK, finalResult.Value)
	}
}

This is more or less the template for all of the handlers, hence a short description:

  1. We ‘make’ a channel so we can put the results on that. This channel has a somewhat complicated type, namely models.OperationResult[[]models.WebEvent] . This means that we can put OperationResult structs on this channel, which have a models.WebEvent slice as their value-type.
  2. Next we try to get a database connection. The GetConnection() method returns the connection and an error if anything went wrong.
  3. We check this error, and the database connection which could also be nil, and return the appropiate JSON
  4. Next we go into the coroutine, which gets a *gin.Context as its parameter, and which is passed a read-only copy of the context, made by c.Copy(). Read-only variables are preferred in a multithreading environment, as they prevent all kinds of race-conditions.
  5. Next we use the *DB Find() method to retrieve all the events, and we store the result of the Find() method in dbResult.
  6. Next the dbResult.Error field is checked and the channel is filled with the appropiate values, either with an error or without
  7. Outside of the coroutine the channel is read into the finalResult.
  8. If there is an error, the appropiate JSON is generated, otherwise the list of events is returned back to the client.

As you can see, using go routines is not very difficult, but requires some good thinking to see how a process is run.

The CreateEvent handler

The CreateEvent handler is more or less the same code:

func CreateEvent(c *gin.Context) {
	result := make(chan models.OperationResult[models.WebEvent])
	var webEvent models.WebEvent
	dbConnection, err := db.GetConnection()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err})
		return
	}
	if dbConnection == nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "No database connection"})
		return
	}
	go func(context *gin.Context) {
		conversionError := c.BindJSON(&webEvent)
		if conversionError != nil {
			result <- models.ConstructWithError[models.WebEvent](conversionError)
		} else {

			dbResult := dbConnection.Connection.Create(&webEvent)
			if dbResult.Error != nil {
				result <- models.ConstructWithError[models.WebEvent](dbResult.Error)
			} else {
				result <- models.ConstructWithoutError[models.WebEvent](&webEvent)
			}
		}

	}(c.Copy())
	finalResult := <-result
	if finalResult.Result != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"message": finalResult.Result})
	} else {
		c.JSON(http.StatusOK, finalResult.Value)
	}
}

The only difference with the ListEvents handler is the BindJson() call, which could cause additional errors, for the rest both handlers, and the rest of the handlers, are pretty much the same.

Conclusion

Making a Web API asynchronous is not very hard in Go. The only thing to look out for is error-handling, since Go, thankfully, does not have exceptions. The solution I give here is not bad, and might even be called elegant, but if you have a better solution for this problem, do not hesitate to contact me.

Leave a Reply

Your email address will not be published. Required fields are marked *