DEV Community

Alexis Moody
Alexis Moody

Posted on

Clean Go Code (Part 1)

For most of my full stack career I've been a Ruby/JS dev working in Rails and React. But over the past 10 months I've dived head first into Golang due to my startup's acquisition. I've always been a big believer in human readable code that follows SOLID principles but I found the Go community tended towards short namespacing and single files with loosely coupled functions. To help improve my understanding of Golang I've decided to document some handy techniques I've learned that help keep your code easy to read and performant. These techniques are in no way one size fits all, but they have helped me in maintaining old and new microservices used by a 200+ engineering team. I hope you find value from this series as well!

Converting Data From One Type To Another

Imagine you are maintaining a social, long form text, posting platform. And let's say you're putting together an API that ingests a filter for a post's status.

Your SQL schema would look something like this:

CREATE TYPE posts.post_status_type AS ENUM ( 'DRAFT', 'PUBLISHED', 'UNDER_REVIEW' ); CREATE TABLE posts.posts ( id bigint NOT NULL, author_id bigint NOT NULL, title character varying(24) NOT NULL, content text, status posts.post_status_type NOT NULL ); 
Enter fullscreen mode Exit fullscreen mode

Your database layer Go struct would look something like this:

package db type DatabasePost struct { ID *int64 Title *string Content *string Status *string AuthorID *int64 } type DatabasePostFilter struct { Title *string Status []*string } func FetchAllPosts( ctx context.Context, authorID string, f *DatabasePostFilter, ) ([]*DatabasePost, error) { // Query the database by formatting SQL  } 
Enter fullscreen mode Exit fullscreen mode

And your api layer structs would look like:

package api_models type ApiPostStatus int32 const ( ApiPostStatusDraft ApiPostStatus = 0 ApiPostStatusPublished ApiPostStatus = 1 ApiPostStatusUnderReview ApiPostStatus = 2 ) type ApiPost struct { ID *int64 Title *string Text *string Status *ApiPostStatus AuthorID *int64 } type GetPostsRequest struct { AuthorID *int64 Filter *GetPostsRequestFilter } type GetPostsRequestFilter struct { Title *string Status []*ApiPostStatus } 
Enter fullscreen mode Exit fullscreen mode

You could have that filter be a single select, but for the sake of this example we'll make it a little more flexible by allowing multiple statuses at the same time. On the SQL side of this filter the single select would result in SQL like:

SELECT * FROM posts WHERE (posts.status = $1); 
Enter fullscreen mode Exit fullscreen mode

where the multiple status filter would look like:

SELECT * FROM posts WHERE (posts.status = $1 OR posts.status = $2); 
Enter fullscreen mode Exit fullscreen mode

Now that we have a firm understanding of our data let's take a stab at converting our API layer status into our database layer status in an API function.

package api // import a database package that contains all of the functions related // to interacting with the database import ( "src/posts/db" "src/posts/api_models" ) type PostsAPI struct { DB db.PostsDB } func (a *PostsAPI) GetPosts( ctx context.Context, r GetPostsRequest, ) ([]*ApiPost, error) { // Convert the ApiPostStatus type to a string type var dbStatusStrings []*string for _, s := range r.Filter.Status { if s == api_models.ApiPostStatusDraft { dbStatusStrings = append(dbStatusStrings, "DRAFT") } if s == api_models.ApiPostStatusPuplished { dbStatusStrings = append(dbStatusStrings, "PUBLISHED") } if s == api_models.ApiPostStatusUnderReview { dbStatusStrings = append(dbStatusStrings, "UNDER_REVIEW") } } // Add the converted type to the filter struct dbPostFilter := DatabasePostFilter{ Title: &r.Filter.Title, Status: &dbStatusStrings, } // Make the db level fetch for all of the author's posts dbPosts, err := db.FetchAllPosts(r.AuthorID, dbPostFilter) if err != nil { return nil, err } // Convert db posts to api posts var posts []*ApiPost for _, dbPost := range dbPosts { apiPost := &api_models.ApiPost{ ID: &dbPost.ID, Title: &dbPost.Title, Text: &dbPost.Text, AuthorID: &dbPost.AuthorID } if dbPost.Status == "DRAFT" { apiPost.Status = api_models.ApiPostStatusDraft } if dbPost.Status == "PUBLISHED" { apiPost.Status = api_models.ApiPostStatusPublished } if dbPost.Status == "UNDER_REVIEW" { apiPost.Status = api_models.ApiPostStatusUnderReview } posts = append(posts, apiPost) } return posts, nil } 
Enter fullscreen mode Exit fullscreen mode

So that works. But you've got 6 if statements, and each one bumps up your cyclomatic complexity.

Separate your concerns

What can we do to increase the readability of this function and decrease the cyclomatic complexity? Let's try separating the concerns by pulling these checks into small helper functions in a converter package.

package converters import "src/posts/api_models" func apiStatusToDBStatus( statuses []*api_models.ApiPostStatus ) []*string { var dbStatusStrings []*string for _, s := range r.Filter.Status { if s == api_models.ApiPostStatusDraft { dbStatusStrings = append(dbStatusStrings, "DRAFT") } if s == api_models.ApiPostStatusPuplished { dbStatusStrings = append(dbStatusStrings, "PUBLISHED") } if s == api_models.ApiPostStatusUnderReview { dbStatusStrings = append(dbStatusStrings, "UNDER_REVIEW") } } return dbStatusStrings } func dbStatusToApiStatus(status *string) *api_models.ApiPostStatus { if status == "DRAFT" { return api_models.ApiPostStatusDraft } if status == "PUBLISHED" { return api_models.ApiPostStatusPublished } if status == "UNDER_REVIEW" { return api_models.ApiPostStatusUnderReview } } 
Enter fullscreen mode Exit fullscreen mode

Then our api function would look like:

package api // import a database package that contains all of the functions related // to interacting with the database import ( ... "src/posts/converters" ) func (a *PostsAPI) GetPosts( ctx context.Context, r GetPostsRequest, ) ([]*ApiPost, error) { dbStatusStrings := converters.apiStatusToDBStatus(r.Filter.Status) // Add the converted type to the filter struct // Make the db level fetch for all of the author's posts // Convert db posts to api posts for _, dbPost := range dbPosts { apiPost.Status = converters.dbStatusToApiStatus(dbPost.Status) } // return converted posts } 
Enter fullscreen mode Exit fullscreen mode

You could probably also move the entire apiPost to dbPost conversion to a converter package function. But for the sake of this exercise we'll leave that alone.

Simplify the approach

This approach is fine and your cyclomatic complexity has definitely gone down. But you're still using a function that essentially maps one static value to another. To me the maintainability of this api would be significantly improved by creating a static map between the database enum strings and the api status type. By storing these values in a variable we reduce the memory we need to allocate for our application and make it easier to guard against bad data.

Let's take a look!

package converters import ... var ApiStatusMap map[api_models.ApiPostStatus]string := { api_models.ApiPostStatusDraft: "DRAFT", api_models.ApiPostStatusPublished: "PUBLISHED", api_models.ApiPostStatusUnderReview: "UNDER_REVIEW", } var DBStatusMap map[string]api_models.ApiPostStatus := { "DRAFT": api_models.ApiPostStatusDraft, "PUBLISHED": api_models.ApiPostStatusPublished, "UNDER_REVIEW": api_models.ApiPostStatusUnderReview, } func ApiStatusToDBStatus( statuses []*api_models.ApiPostStatus ) []*string { var dbStatusStrings []*string for _, s := range r.Filter.Status { if dbStatus, ok := ApiStatusMap[s]; ok { dbStatusStrings = append(dbStatusStrings, dbStatus) } } return dbStatusStrings } func DbStatusToApiStatus(status *string) *api_models.ApiPostStatus { return DBStatusMap[status] } 
Enter fullscreen mode Exit fullscreen mode

By defining static conversion maps we reduce the complexity of our code while also improving the maintainability. If a new status type is added we can add 2 new lines to our maps instead of upwards of 6 lines to 2 different functions. In fact, we could probably remove the DbStatusToApiStatus function and use the DBStatusMap in our api function like:

 var posts []*ApiPost for _, dbPost := range dbPosts { apiPost := &api_models.ApiPost{ ID: &dbPost.ID, Title: &dbPost.Title, Text: &dbPost.Text, AuthorID: &dbPost.AuthorID } if apiStatus, ok := converters.DBStatusMap[dbPost.Status]; ok { apiPost.Status = apiStatus } posts = append(posts, apiPost) } 
Enter fullscreen mode Exit fullscreen mode

I hope this technique helps you when converting custom types. Feedback is always welcome and be sure to look out for the next post in this series soon!

Top comments (0)