A Starter's Guide To Go Projects

by Artur Kondas

Table of contents

  • Why should I even care?
  • How big players do it?
  • How I can do it?
  • Example: Small API

Why should I even care?

Chances are you've said some of these things before:

  • I don't want to do anything specific with this package...
  • I just want to test some things...
  • I don't care about it, because it's a private repo...

But what if this code will turn your life around?

Before we answer all the burning questions, let's check how big players structure their code

Project Layout by Golang Standards

Most probably if you start Googling the topic, you'll find this repo first. This is the most popular way of structuring the content in Go projects.

Kubernetes

Terraform

Can you see any pattern?

As you can see, a lot of these repositories are very similar in the structure.

Does it mean it's the only valid way?

Let's think about our projects

How to tackle the structure depending on the initial size

1. Flat Structure

Best for

  1. Small projects
  2. Testing ideas
  3. Packages

How should it look?

Small project and testing ideas

              
                |
                | go.mod 
                | go.sum
                | main.go
                | functionality.go
                | functionality_test.go
                | /assets
                | README.md
                |
              
            

Packages

              
                |
                | go.mod 
                | go.sum
                | package.go
                | package_test.go
                | functionality.go
                | functionality_test.go
                | README.md
                |
              
            
  • Try to steer away from using utils.go, most of the time it'll end up being a mess. As much as this is counter-intuitive, it'll make you think about the overall structure of the app.
  • If your files are getting awfully big, split them into multiple ones grouped by functionality. For example: postgres.go, json.go.
  • Also, think about README.md from the very beginning. Good documentation will save you multiple times!

2. Main app and packages

Best for

  • Basically anything else

Before diving into this, ask yourself one question:

Will the packages be reused across other apps?

If yes, it makes a perfect sense to split them to separate repositories

If no, then we can follow the Big Players setup

                  
                    |
                    | /pkg
                    | /build
                    | /examples
                    | go.mod 
                    | go.sum
                    | main.go
                    | README.md
                    |
                  
                
  • Keep the source code directories as simple as possible, don't create all folders at once - extend only when you need
  • Use /pkg for all packages used in the application only;
  • Use /build for any build scripts;
  • /examples should be used if your app has some very specific functionality you want to highlight;

Go was created as an alternative to big C++/Java apps. Treat it like that!

If you can keep it simple - do it.

You don't have to structure your Go code in the same way as Kubernetes or Docker does.

That's their use case - think in baby steps and expand as you Go.

Example: Small API

Requirements:

  • Logger and Database connector is reused in other apps
  • Keys validator is used only in the API

Main app

                
                    | /pkg
                    |   /app
                    |     app.go
                    |     app_test.go
                    |     handlers.go
                    |     handlers_test.go
                    |   /keys
                    |     keys.go
                    |     keys_test.go
                    | /build
                    |   dockerfile
                    |   docker-compose.yml
                    | go.mod 
                    | go.sum
                    | main.go
                    | README.md
                
              
  • /app is the main directory for the application with it's logic.
    app.go holds the app startup logic, router and db initialisation.

    handlers.go as the name says, is the API handlers logic.

  • keys.go is the package that will check the environment setup before the application runs. This is a very specific use-case!
  • /build holds all the files required for any type of build.
  • main.go in this case contains only the barebones required for the application starup

main.go

              
package main

import (
  "log"

  "github.com/account/app/pkg/app"
  "github.com/account/app/pkg/keys"
)

func main() {
  keys.Check(keys.Keys{
    App:      true,
    Postgres: true,
    Logger:   true,
  })
  a := app.App{}
  err := a.Initialize()
  if err != nil {
    log.Fatal(err)
  }
  err := a.Run()
  if err != nil {
    log.Fatal(err)
  }
}
              
            

app.go

              
package app

import (
  "github.com/gofrs/uuid"
  "github.com/gorilla/mux"
)

type App struct {
  Router   *mux.Router
  Database Database
}

type Database interface {
  LoginUser(email string) (types.User, error)
  GetUser(userId uuid.UUID) (types.User, error)
  CreateUser(user types.User) error
  ModifyUser(user types.User) error
  DeleteUser(userId uuid.UUID) error
}
              
            

handlers.go

              
func (a *App) UserGet() http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    uid, err := uuid.FromString(vars["user_id"])
    if err != nil {
      JSONResponse(w, http.StatusBadRequest, payload)
      return
    }
    defer r.Body.Close()

    user, err := a.Database.GetUser(uid)
    if err != nil {
      JSONResponse(w, http.StatusBadRequest, payload)
      return
    }

    JSONResponse(w, http.StatusOK, payload)
  })
}
              
            

Logger

                
                    | go.mod 
                    | go.sum
                    | logger.go
                    | README.md
                
              

logger.go is basically a wrapper around Uber's Zap with some of my custom setup, depending on the stack.

Database

                
                    | go.mod 
                    | go.sum
                    | database.go
                    | user.go
                    | README.md
                
              
  • database.go holds all the logic for initial database connection - in Postgres' case it'd build the connection string and build some functions for checking if the database is alive.
  • user.go holds the functions that will fetch the data from the database and return it in a proper fashion to the application for handlers to return it to the user.

In Conclusion

  • Expand as you Go, don't overthink it from the start
  • Use the structure that makes sense for your app
  • Think about the overall architecture
  • Have fun!

About Me

Go Engineer @ ECS Digital

Find Me:

Me