Golang is quirky
So recently I had the opportunity to make a proof of concept (PoC) in the Go language to share with some fellow developers. They are mostly Java developers and therefore I decided to make the PoC a MVC style REST API that returns JSON with a single Entity for now.
It does however store everything in a Postgresql database and features the full suite of HTTP verbs; GET, POST, PUT, PATCH and DELETE. With GET both returning the single entity and the list of all entities. I did not make pagination and transactions in the database part as it was not necessary for this PoC.
Syntax
The syntax still is quirky but now I know more of why it is the way it is. Everything that is Pascal case (meaning uppercase letters on the first words, ChocolateChipCookie for instance) will be public to other parts of the code. Everything that is camel cased (meaning the first letter is lowercase so chocolateChipCookie) will not be public to other parts of the code that exist in other packages. This explains a lot, as this way of making the formatting determine the accessibility levels shapes the way the code looks. This goes for methods, structs and anything else.
Not only do they need to be named as such, they also have to be foreseen of a comment. This I felt led to slowdown in the development cycle and a lot of redundant stupid comments that just annoyed me. If you name it Pascal case but no comment it will not be exported, which is the vernacular in Go to mean it is a public method. This also means that in a struct that will be used to return a response DTO you have to name everything with Pascal case and then use so called tags to shape the JSON data again.
Formatting
There is an auto formatting tool used on your code and it has to conform to that format otherwise you cannot compile. This is not too bad however if it conflicts with your own style then you have a problem.
Imports
You cannot have unused imports and they will be removed automatically for you. This is quite annoying to say the least. I want to have the module imported in order to see what is inside it. This fights me on so many occasions. So you have to import something make a stupid variable to keep it around and look into it then.
Errors
The error handling sucks. You have two extremes, panic
and Errors.New
. The panic
stops the whole program, kind of like a segfault or Runtime exception in Java. The Errors.New
has to bubble up through all the methods, so almost every method will return the data and the error. Then you have to manually check it. There is no problem with the following code:
⋮
data, _ := db.Find(&fruit)
⋮
Which means you ignore the error and carry on which is not what you want maybe. It also means you can inadvertently do this, without realizing it and then get some weird errors in production.
Packages
The packages are directory based only. So you can only have one package declared in one directory. You can have many files in one directory all belonging to the same package but the package is the identifier and not the file name like in Java. This required some getting used to. I also feel you miss some control somehow, as I ran into the fact I wanted to have a entity
package but within it you will have all the entity straight by name. There does not exist a nested package where you type in entity.fruit.Fruit
for example, then it will just be fruit.Fruit
and it will come from the folder entity/fruit/
for example. Under water you can have multiple naming of fruit
just coming from different areas. I also inverted the naming, so making fruit
carry all the different proponents like service
and dao
making it easier to deal with it.
Getting started
The whole getting started is getting a go.mod
file by executing go mod init <name>
and then you are off. The only slight downside is you have to manage your own GOPATH
environment variable to make a sort of working virtualenv like Python.
Idiomatic
There is no idiomatic way of writing Golang. There are rules for style and grammar and the like but they do not dictate how you write the overall architecture of the Golang application itself. It is difficult to get consistency across the ecosystem and therefore you get the same problem as in React for example and PHP as well. There will be subgroups and everyone is left to their own devices on how to do things.
That something is considered Pythonic is a good thing. There should optimally be only one clear way to do something. That means across the whole application and across the whole language ecosystem.
Classes and methods
There exists no such thing as a class in the Go language. What you do have are called structs
and they can contain methods on them. So you construct the full class and methods and internal and external variables that are accessible by making a struct. A quick example of how to translate a class called Message
with a constructor that takes in one argument called msg
which is a string would be:
type Message struct {
Msg string
}
To give that class the method display
you would do the following:
func (m *Message) Display() {
fmt.Println(m.Msg)
}
There exists no such thing as this
or self
either, so you have to use the named way of adding methods to structs.
Things left to be discovered
The things I yet have to touch on and discover are the goroutines and the channels. These things I will uncover in a future post about CQRS where I will turn the following application into a CQRS one.
The PoC itself
So below is the PoC itself and the structure, there is not a lot going on as it is an easy application. I did try to get as much of the functionality in here of the Go language itself as that was the goal to showcase as much as possible.
├── controllers
│ ├── controllers.go
│ └── fruit
│ └── fruit.go
├── dist
│ └── gin
├── docker-compose.yaml
├── Dockerfile
├── go.mod
├── go.sum
├── main.go
├── models
│ ├── dto
│ │ └── fruit.go
│ ├── entity
│ │ └── fruit.go
│ └── models.go
├── pkg
│ └── mod
│ └── cache
│ └── lock
├── router
│ └── router.go
└── services
├── dao
│ ├── dao.go
│ └── fruit
│ └── fruit.go
├── fruit
│ └── fruit.go
└── services.go
The folders dist
and pkg
can be ignored for now. We will start with the go.mod
one which contains the following.
module pocs/go/gin
go 1.15
require (
github.com/gin-gonic/gin v1.6.3
github.com/google/uuid v1.1.1
github.com/jinzhu/gorm v1.9.16
)
I named it gin too, which is mainly because that is the framework I used to make the web app this time. The last name of the whole thing is what Go will call it. Therefore there is a gin
executable in the dist folder.
Then the go.sum
is just there for the checksums. The docker-compose.yaml
is there to help the devs out a bit more.
version: "3.6"
networks:
gin_connect:
name: "gin_connect"
services:
gin:
image: golang:1.15-alpine3.12
container_name: gin.go.local
env_file:
- .env/app
volumes:
- .:/srv/http
command: "ash -c 'cd /srv/http && go run --tags=jsoniter .'"
depends_on:
- db
networks:
- gin_connect
stdin_open: true
tty: true
ports:
- 127.0.0.1:9999:8080
db:
image: postgres:13-alpine
container_name: db.go.local
environment:
POSTGRES_PASSWORD: go
POSTGRES_USER: go
POSTGRES_DB: go
stdin_open: true
tty: true
networks:
- gin_connect
Then the Dockerfile
is there to make the “actual” docker image that will be run in the cloud environments.
FROM golang:1.15-alpine3.12 as builder
COPY . /srv/http
RUN cd /srv/http && go build --tags=jsoniter .
FROM alpine:3.12
COPY --from=builder /srv/http/dist/gin /app
CMD ["/app"]
Actual code
So let us get into the actual code. This is what the main.go
file looks like:
package main
import (
"pocs/go/gin/router"
"pocs/go/gin/services"
)
func main() {
r := router.Router()
services.SetupDatabase()
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
Every Go application looks for one function called main in the main package. Packages are directory based so the root of this application is the main package. It just sets up the Router and the database. Also note that main is private as it is lowercased.
The router package is located in router.go
.
package router
import (
"github.com/gin-gonic/gin"
"pocs/go/gin/controllers"
)
// Router sets up the complete routes from controllers and returns the full router
func Router() *gin.Engine {
router := gin.Default()
controllers.Init(router)
return router
}
I chose to go the route of having the controllers package itself supply the routes by making each controller supply an Init method.
First the database though. It is in services.go
.
package services
import (
"pocs/go/gin/models/entity"
"pocs/go/gin/services/dao"
)
// SetupDatabase makes sure all schemas are present
func SetupDatabase() {
db := dao.Database()
db.AutoMigrate(&entity.Fruit{})
}
It just opens a connection to the database and does AutoMigrate for the one entity we have which makes sure the schema is there. This is a nice feature, but I have no idea how changes are managed yet.
Then the database comes from dao.go
.
package dao
import (
"fmt"
"os"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres" // Blank import for wrapping GORM
)
// Database returns a database connection
func Database() *gorm.DB {
host, user, pass, dbname := os.Getenv("DB_HOST"), os.Getenv("DB_USER"), os.Getenv("DB_PASS"), os.Getenv("DB_NAME")
ssl := "disable"
_, ok := os.LookupEnv("DB_SSL")
if ok {
ssl = "require"
}
db, err := gorm.Open("postgres", fmt.Sprintf("host=%s port=5432 user=%s dbname=%s password=%s sslmode=%s", host, user, dbname, pass, ssl))
if err != nil {
panic(err)
}
return db
}
This package just connects to the database. I have no idea yet if gorm.Open
gives back a connection pool or that you just open and close and that underwater the open will just have a internal access to a connection pool. I have to delve into that one a bit more*. For now though it is just a PoC so this works for just one entity and no active users to one at most at a time.
*Found out the underlying mechanism and indeed one call to Open will return a connection pool.
Models
Now that the database is out the way let us talk about the models. I use one Entity to represent the database records themselves and some DTOs. models.go
just contains some empty package declaration. I created this as I was debugging the reason why it could not find my package. It turned out it was how I was calling it in the controller that was causing the issue. This file might be removed altogether.
package models
Then there is fruit.go
inside entity.
package entity
import (
"time"
"github.com/google/uuid"
)
// Fruit represents the fruit in the database
type Fruit struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Name string
}
It just contains some standard stuff. The reason why DeletedAt
is a pointer, is because then it can be nil
. Standard GORM only supports integers as ids. This is a bad idea I feel, so therefore it is now a uuid. That does mean some manual action on certain actions, that is okay. Next up are the DTOs inside fruit.go
in dto.
package dto
import (
"pocs/go/gin/models/entity"
"github.com/google/uuid"
)
// ReadFruit is the DTO for getting fruit at GET /fruit/:id
type ReadFruit struct {
ID string `uri:"id" binding:"required,uuid"`
}
// CreateFruit is the DTO for creating new fruit at POST /fruit
type CreateFruit struct {
Name string `json:"name" binding:"required,max=255"`
}
// UpdateFruit is the DTO for creating new fruit at POST /fruit
type UpdateFruit struct {
Name string `json:"name" binding:"required,max=255"`
}
type metadata struct {
CreatedAt int64 `json:"created_at" `
UpdatedAt int64 `json:"updated_at" `
}
// ResponseFruit is the DTO for all responses
type ResponseFruit struct {
ID uuid.UUID `json:"id" `
Name string `json:"name" `
Metadata metadata `json:"metadata" `
}
// Single creates a single ResponseFruit from a entity
func (res *ResponseFruit) Single(fruit entity.Fruit) *ResponseFruit {
return &ResponseFruit{ID: fruit.ID,
Name: fruit.Name,
Metadata: metadata{
CreatedAt: fruit.CreatedAt.Unix(),
UpdatedAt: fruit.UpdatedAt.Unix()}}
}
// Multi creates an array of responses
func (res *ResponseFruit) Multi(fruits ...*entity.Fruit) []*ResponseFruit {
result := make([]*ResponseFruit, cap(fruits))
for i := range fruits {
fruit := fruits[i]
result[i] = res.Single(*fruit)
}
return result
}
This contains the DTO necessary for holding certain bindings in order to utilize gin functions in the controller that will check if you supplied the correct data. These occur in the form of so called tags. These are the string after the type declaration inside the struct. Then I added two helper methods, Single
and Multi
. They take in a entity and convert them into a ResponseFruit
DTO. The ... means a variadic input, so it can be 0,1 or any other whole positive integer.
Controllers
The controllers start with the uniform entry point in controllers.go
.
package controllers
import (
"github.com/gin-gonic/gin"
"pocs/go/gin/controllers/fruit"
)
// Init takes router and calls all subsequent init methods
func Init(router *gin.Engine) {
fruit.Init(router)
}
This will be extended on later of course. Then the only one we have right now is fruit.go
inside of the controllers.
package fruit
import (
"net/http"
"reflect"
"github.com/gin-gonic/gin"
"pocs/go/gin/models/dto"
"pocs/go/gin/services/fruit"
"github.com/google/uuid"
)
// Init takes router and adds the necessary routes to it
func Init(router *gin.Engine) {
group := router.Group("/fruit")
{
group.GET("/", listHandler)
group.GET("/:id", getHandler)
group.POST("/", postHandler)
group.PATCH("/:id", patchHandler)
group.PUT("/:id", putHandler)
group.DELETE("/:id", deleteHandler)
}
}
var service = &fruit.Service{}
var listHandler = func(c *gin.Context) {
fruits, err := service.FindAll()
if err != nil {
c.SecureJSON(http.StatusNotFound, gin.H{"msg": err.Error()})
return
}
var res = dto.ResponseFruit{}
c.SecureJSON(http.StatusOK, gin.H{"data": res.Multi(fruits...)})
}
var getHandler = func(c *gin.Context) {
var req dto.ReadFruit
if err := c.ShouldBindUri(&req); err != nil {
c.SecureJSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
return
}
fruit, err := service.Find(uuid.MustParse(req.ID))
if err != nil {
c.SecureJSON(http.StatusNotFound, gin.H{"msg": err.Error()})
return
}
var res = dto.ResponseFruit{}
c.SecureJSON(http.StatusOK, gin.H{"data": res.Single(*fruit)})
}
var postHandler = func(c *gin.Context) {
var req dto.CreateFruit
if err := c.ShouldBindJSON(&req); err != nil {
c.SecureJSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
return
}
fruit, err := service.Create(req.Name)
if err != nil {
c.SecureJSON(http.StatusInternalServerError, gin.H{"msg": err.Error()})
return
}
var res = dto.ResponseFruit{}
c.SecureJSON(http.StatusOK, gin.H{"data": res.Single(*fruit)})
}
var patchHandler = func(c *gin.Context) {
var uriCheck dto.ReadFruit
var reqBodyCheck dto.UpdateFruit
if err := c.ShouldBindUri(&uriCheck); err != nil {
c.SecureJSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
return
}
if err := c.ShouldBindJSON(&reqBodyCheck); err != nil {
c.SecureJSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
return
}
obj, err := service.Find(uuid.MustParse(uriCheck.ID))
if err != nil {
c.SecureJSON(http.StatusNotFound, gin.H{"msg": err.Error()})
return
}
elem := reflect.ValueOf(&reqBodyCheck).Elem()
typeOf := elem.Type()
for i := 0; i < elem.NumField(); i++ {
f := elem.Field(i)
v := reflect.ValueOf(obj).Elem().FieldByName(typeOf.Field(i).Name)
if v.IsValid() {
v.Set(f)
}
}
fruit, err := service.Update(*obj)
if err != nil {
c.SecureJSON(http.StatusInternalServerError, gin.H{"msg": err.Error()})
return
}
var res = dto.ResponseFruit{}
c.SecureJSON(http.StatusOK, gin.H{"data": res.Single(*fruit)})
}
var putHandler = patchHandler
var deleteHandler = func(c *gin.Context) {
var req dto.ReadFruit
if err := c.ShouldBindUri(&req); err != nil {
c.SecureJSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
return
}
_, err := service.Delete(uuid.MustParse(req.ID))
if err != nil {
c.SecureJSON(http.StatusNotFound, gin.H{"msg": err.Error()})
return
}
c.SecureJSON(http.StatusNoContent, nil)
}
There is a lot of repetition. So for the future I would definitely want to utilize some compression oriented programming and take those out into more private methods.
Services
Then we get to the service in fruit.go
inside services/fruit.
package fruit
import (
"pocs/go/gin/models/entity"
"pocs/go/gin/services/dao/fruit"
"github.com/google/uuid"
)
// Service is there to hold everything and have multiple copies
type Service struct {
}
var dao = &fruit.DAO{}
// Find gives back the Fruit with given uuid if it exists
func (s *Service) Find(id uuid.UUID) (*entity.Fruit, error) {
return dao.FindByID(id)
}
// FindAll gives back the Fruit with given uuid if it exists
func (s *Service) FindAll() ([]*entity.Fruit, error) {
return dao.Find()
}
// Create will assign random uuid and set createdAt date
func (s *Service) Create(name string) (*entity.Fruit, error) {
fruit := entity.Fruit{Name: name, ID: uuid.New()}
return dao.Insert(fruit)
}
// Update updates the resource if it can find it
func (s *Service) Update(fruit entity.Fruit) (*entity.Fruit, error) {
return dao.Update(fruit)
}
// Delete soft deletes the record
func (s *Service) Delete(id uuid.UUID) (*entity.Fruit, error) {
var fruit = entity.Fruit{}
fruit.ID = id
return dao.Delete(fruit)
}
Just a bunch of helper methods for doing CRUD. Then I also made a DAO layer just to help out the Java devs a bit more. It is inside fruit.go
in services/dao/fruit.
package fruit
import (
"fmt"
"pocs/go/gin/models/entity"
"pocs/go/gin/services/dao"
"github.com/google/uuid"
)
// DAO struct that will hold the methods for doing DAO stuff
type DAO struct {
}
var db = dao.Database()
// Find gets all fruits without pagination
func (DAO) Find() ([]*entity.Fruit, error) {
var fruits []*entity.Fruit
result := db.Find(&fruits)
if result.Error != nil {
return nil, result.Error
}
return fruits, nil
}
// FindByID helper method to call internal find method
func (DAO) FindByID(id uuid.UUID) (*entity.Fruit, error) {
fruit := entity.Fruit{}
result := db.Where("id = ?", id.String()).First(&fruit)
if result.RecordNotFound() {
return nil, fmt.Errorf("Fruit with id: %s does not exist", id)
}
return &fruit, nil
}
// Insert will insert record
func (d *DAO) Insert(fruit entity.Fruit) (*entity.Fruit, error) {
result := db.Create(&fruit)
if result.Error != nil {
return nil, result.Error
}
return d.FindByID(fruit.ID)
}
// Update will update the record in the database
func (d *DAO) Update(fruit entity.Fruit) (*entity.Fruit, error) {
result := db.Save(&fruit)
if result.Error != nil {
return nil, result.Error
}
return d.FindByID(fruit.ID)
}
// Delete will soft delete the record in the database
func (d *DAO) Delete(fruit entity.Fruit) (*entity.Fruit, error) {
_, err := d.FindByID(fruit.ID)
if err != nil {
return nil, err
}
result := db.Delete(&fruit)
if result.Error != nil {
return nil, result.Error
}
db.Unscoped().Find(&fruit)
return &fruit, nil
}
The reason the extra FindByID
calls occur are because the Save
and Create
methods from GORM do not update the actual representation sent in, that only occurs with the Find
method. By default Find
will not look for soft deleted records, therefore the Delete
uses the Unscoped
variant. The Service and DAO layers are consistent in that they all return the entity but I just do not do anything with it in the controller for the DELETE
verb.
Conclusions
That is it for the PoC. Just a simple application that hopefully showcases the Go language. It is quirky at times but you get used to it and also there exist enough good libraries out there that you can use to make web application dev possible. I do think it is best used for the things it is being used for right now, which is to make CLI tools that will run everywhere. Like Docker and Kubernetes as well as a webserver called Caddy that can run anywhere.
The thing that I dislike the most is the error handling, or lack thereof. It feels very janky that you have to manually check and bubble errors all over the place. Almost every method returns the error again. This means it is quite cumbersome and you keep writing the same code over and over again.
Also the structure of the Go application is a bit arbitrary, so you can go all out like I did here or just put it all together and keep things as private as possible everywhere. I think this is just all preference and you should make good agreements within the team on what will be the way forward.