Building a Go Web App – How to Start With Go

This article should help you get off to a great start with your new project in Go.

Last Update

08 Jan, 2021

Reading Time

7 Min

Marcin DrykaCTO
Github LinkLinkedin Link
Olga GierszalEditor
Linkedin Link


Once you’ve started working on your Go web app, there are plenty of unanswered questions. Some of them come up even before the first line of source code is even written.

Let’s make the beginning of your journey with Go a little bit easier. Below, find answers to the most burning questions about frameworks, DI containers, and choosing directory structure.

Which framework should I choose for my Go web app?

When selecting a tool to work with, it is often the case that we have to decide between speed and the richness of various features.

It is no different when choosing a framework in a new programming language. There are too many solutions and it’s not possible to try all of them.

In the Go language you can find over 40 frameworks for creating Go web applications. How do you choose the right one for your Go application?

Check the popularity

First of all, let’s check which of them are actively developed. This shortens the list significantly, although not enough to choose just one. In such cases, checking the popularity of a tool in Github helps.

The most popular Golang framework has 32k stars. So you could say that those without at least 6k (20% of the maximum score) aren’t very popular, which may later create some problems with finding a team of programmers, community support, etc.

Most popular Go frameworks

There are 4 tools with more than 6k stars:

Time to choose

With a list that short, you can spend some time reviewing each one thoroughly. Check if the framework provides the tools you will need:

  • What does the data validation look like?
  • What about Websockets?
  • What does the router look like?
  • How do you write the code?

Tip: In general, it’s essential to find out the strengths and weaknesses of the solution.

If each of the tools meets our expectations, there’s nothing standing in the way of starting with the most popular one: gin!

Is it worth using a DI container and if so, which one?

While writing code, it’s a standard to make sure that its structures and functions are not related to other parts of the system, in order to give them flexibility, and to be able to test, develop and modify them.

Undoubtedly, the Dependency Injection containers help to achieve this goal. They allow you to orchestrate the whole so as to build the necessary dependencies.


Wire and Dig are common tools in the Go environment. The first one is used to manage dependencies at the compilation time and the latter is an example of a container in runtime. The downside is that although big companies support them, they are not particularly popular.

Dependencies need to be injected

Regardless of whether you choose the version “with” or “without” the container, it’s important to build the application so that these dependencies are injected. Even without a container, it is possible to achieve this goal.

Here’s an example fragment of an application:

repository := repositories.NewInMemoryParcelRepository()
   parcelService := services.NewParcelService(repository)
   parcelController := controllers.NewParcelController(parcelService)
   server.POST("/schedule", parcelController.ScheduleParcelPickUp)
type ParcelController struct {
   service services.ParcelService
func NewParcelController(service services.ParcelService) ParcelController {
   return ParcelController{service: service}
func (controller ParcelController) ScheduleParcelPickUp(c *gin.Context) {

ParcelController needs ParcelService to work properly, but instead of creating an instance of ParcelService inside the controller, we inject the instance of the already built ParcelService to NewParcelController. This way you can change ParcelService independently of ParcelController.

Finally, we connect the ScheduleParcelPickPickUp method to the router (POST, “/schedule”) with the dependencies we already injected.

Start simple

As long as the project isn’t too big, managing dependencies this way is relatively simple.

Before you decide to include a DI container in your dependency management work, you can try to build dependencies in Builders or Factories. A rather large Go application can be managed without additional tools.

Which directory structure should I choose for my Go web app?

This is a very complicated subject. It depends on the general architecture (monolith vs. microservices), the number of features that your Go application will contain, the complexity of the domain, in both terms: the number of models as well as the domain actions that happen and many other factors that need to be considered.

The Onion Architecture

But what if you just want to build a Go application, and you don’t yet have detailed knowledge about the domain but still need to start with something?

One of the options is to follow the Onion Architecture which is presented below:

├── go.mod
├── go.sum
└── src
    ├── controllers
    │   └── parcel.go
    ├── domain
    │   ├── commands
    │   │   └── ScheduleParcelPickUp.go
    │   ├── models
    │   │   └── parcel.go
    │   ├── repositories
    │   │   └── parcel.go
    │   └── services
    │       └── parcel.go
    ├── features
    │   ├── context
    │   └── main.feature
    │   └── parcel.feature
    ├── go_test.go
    ├── infrastructure
    │   └── repositories
    │       └── parcel.go
    └── main.go

Readme – This is a place where, apart from basic things like logo, project name, headline, badges and description, you can find further information like: installation, usage, configuration, and last but not least, the license when you are planning to publish it as open source (otherwise nobody can use the software!)

go.mod, go.sum – the files used by Go Modules, the standard way of managing dependencies in a Go application.

src/controllers – this is where the user’s http request comes in. The controller is the entrance to our Go application. The controller receives and checks data from the user, asks the application to perform an action, and then returns the answer to the user.

// ParcelController contains parcel schedule related routes
type ParcelController struct {
   service services.ParcelService
// ScheduleParcelPickUp creates a new parcel pick up to courier
func (controller ParcelController) ScheduleParcelPickUp(c *gin.Context) {
   var command commands.ScheduleParcelPickUp
   if err := c.ShouldBindJSON(&command); err != nil {
       c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid form", "form": err})
   parcel := controller.service.ScheduleParcelPickUp(command)
   c.JSON(http.StatusOK, &parcel)

src/domain/commands – are commands given by the user to execute changes in the domain. Controllers convert the http request data into objects containing the user’s intentions.

// ScheduleParcelPickUp very very simple pick up schedule object.
// We assume everyone knows everyone, so the firstname is complete information to pick up parcel.
type ScheduleParcelPickUp struct {
   FirstName string `json:"first_name" binding:"required,min=2,max=100"`

src/domain/services – this is where the magic of the models happens. They interact with each other, changing the state of our Go application. In the case of ScheduleParcelPickPickUp the action is very simple, only take note of this when writing into the database. During the development of business logic, this is where the next actions, such as ParcelPickUp, will go.

// ParcelPickUp changes the parcel to "In delivery" state
func (service ParcelService) ParcelPickUp(data commands.PickUpParcel) (*models.Parcel, error) {
   parcel, err := service.repository.Find(data.ID)
   if err != nil {
       return nil, fmt.Errorf("Cannot pick up nonexisting parcel")
   parcel.Status = "in_delivery"
   return parcel, nil

src/domain/models – are our modeled objects. They represent actual entities in the project.

src/domain/repositories – is the database access layer (only interfaces to enable Inversion of Control). To meet the basic requirements for sending parcels, our repository will be very simple.

type ParcelRepository interface {
   Find(models.ParcelID) (*models.Parcel, error)

It only contains methods that can save and find a parcel. In the future, methods like FindUndeliveredParcels() or FindParcelsDeliveredBy(models.Courier) may appear here.

src/infrastructure/repositories – is a concrete implementation of repository interfaces.

In the example Go application project, we don’t care about the persistence of the data, but about the speed of operation and making the whole example easier. Therefore, the implementation is based on memory (InMemoryRepository), but in the future there will certainly be other implementations (e.g. PostgresRepository).

Using this approach can defer the decision on how to access the database. The more we know before making a decision, the more likely it is the correct one (which ORM or raw queries)

src/features – describing features in BDD style is a topic in itself. There is a library called Godog in Go. It allows us to describe the documentation, and then automatically test our domain based on those files.

src/features/context – Contexts are files that translate a literal description into a specific action in the code.

Feature described as:

Feature: Healthcheck
 In order to have healthy services
 As a container orchestrator
 I need to be able to ping service
 Scenario: Healthcheck
   When I ping
   Then I get pong

This perfectly illustrates the need that a given endpoint meets and what the desired action is, although the implementation is hidden.

Underneath, FeatureContext describes what these steps mean:

func (context *FeatureContext) iPing() error {
   req, _ := http.NewRequest("GET", "/ping", nil)
   context.server.ServeHTTP(context.recorder, req)
   return nil
func (context *FeatureContext) iGetPong() error {
   body := context.recorder.Body.String()
   expected := "{"message":"pong"}"
   if body != expected {
       return fmt.Errorf("Expected to get {"message":"pong"}, but got %s", expected)
   return nil

If you are building an API, then you should also consider saving the technical documentation in this way.

Feature: Healthcheck
 In order to have healthy services
 As a container orchestrator
 I need to be able to use ping endpoint
 Scenario: Healthcheck
   When I send GET request to "/ping"
   Then the response should match pattern:

src/main.go – the main file of our Go application. It contains two functions. The `bootstrap`, which builds all the dependencies and connects the controllers to specific routes, and the `main`, which starts the server.

Your first web app with Go

In order to write an application in a new, unfamiliar language, you should first find answers to a few questions.

In the Go language, to start your adventure with web applications, consider Gin as an HTTP framework. It is popular, and it contains the necessary tools for handling requests without imposing optional choices such as ORM or directory structure. If you don’t know whether you need a DI container, you don’t have to use it from day one.

Start with the rule that you inject dependencies to the lower layers of the Go web app. When you decide that you need a DI container, you will be able to add it to the project.

Be sure to remember to take care of the proper directory structure from the beginning. Don’t forget, however, to take care of the proper structure of the application from the beginning as changing the structure on a working project is difficult and expensive.

Keep me updated

We'll be adding new in-depth content to this handbook very soon. Get it straight to your inbox.