How I Structure Go Code After 2 Years Working in Industrial Programming

Danny Aguswahyudi
6 min readJan 3, 2019

--

Photo by Maximilian Weisbecker on Unsplash

Currently I’m working on a growing start up company as a software engineer. For day to day basis I need write golang code that able to serve a highly mutable business requirement. Since we know that business is never converge and it is only diverge linearly as the business grows.

If you are on the same path as me, I believe you also notice that people (engineers) are often come and go. The code on that company also outlives a single engineer as well. Those problems may describe our work environment, an industrial context.

Industrial programming means writing code once and maintaining it perpetuity. What I mean with maintenance here is a practice of reading and refactoring the code. Based on Peter Bourgon talks about industrial programming, I learnt that industrial programming favor reads and dependency injection is a powerful tool to optimize for read comprehension.

So here is some basic knowledge that I used to implementing dependency injection.

Dependency Injection (non-container DI)

Dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). For example if you have an object that may need a database client to run a function, that means your object is dependent to a database object. So instead of creating a new database connection every time you are going to call that function, we injecting a database object once as a parameter, so that function don’t need to recreate it over and over.

By implementing this technique, I intent to decouple objects and let the new engineer that join the team can easily understand that the Resource(may be a package in real case) is dependent to a database connection in order to properly running it. Before going to dependency injection, here is an example of simple golang object that depend on database object.

package resourceimport "database/sql"

// Resource is type that being used as an object to receive
// dependency
type Resource struct {
db *sql.DB
}

// New is constructor function that do the injection to Resource
func New(db *sql.DB) *Resource {
r := &Resource{
db: db,
}

return r
}

// PingDB is method receiver from type Resource which using
// database injection from constructor func
func (r *Resource) PingDB() error {
return r.db.Ping()
}

On the snippet above we can see that the constructor function New() receive a database object as parameter to create object Resource. This constructor function can help us to determine what are Resource need to properly run and it help us to check any error in compile time if there is addition/deletion of parameters.

Interface

An interface type is defined as a set of method signatures. A value of interface type can hold any value that implements those methods. Here is simple example about how interface is going to be useful for implementing dependency injection.

package main

// Dog struct
type Dog struct{}

// Speak is receiver method from Dog struct
func (d *Dog) Speak() string {
return "woof"
}

// Animal interface will be like a contract that going to
// implement any animal that implementing Speak() method
type Animal interface {
Speak() string
}

func main() {
var a Animal
a = &Dog{} // example of using Dog as an Animal

fmt.Println(a.Speak())
}

On example above, interface Animal can receive any kind of type as long as that type has a receiver method Speak() string. The benefit of this way, we can make a loose coupled code since we don’t need to know what really happen on the (d *Dog) Speak()or I can say the injector. More over we can generate a mock function through the interface Animal as well using gomock. With those capability it would be more easier to our service to test a specific function since we are using declared interface to create mocking and be more focusing to test another business logic.

Implementing Dependency Injection

Based on the knowledge earlier, I implementing dependency injection into three part which are Injector, Service, Resource. For simplicity and learning purpose, I will be using a case where there is a user going to make an order in marketplace.

Simple flow

Resource is a part where all the call to dependencies (database, redis, etc) happen. The call it self will be invoked by injecting the dependency to resource as parameter on constructor func and make this part can be using a single connection over and over (in case of database connection). I implement all the stuff that related to mutating data to resource part.

package orderimport "database/sql"// Resource is struct that containing of package user dependencies
type Resource struct {
db *sql.DB
}
// New is constructor function to inject dependency to user resource
func New(db *sql.DB) *Resource {
return &Resource{
db: db,
}
}
// CreateOrder do insert order data to database
// Will return error when no record found
func (r *Resource) CreateOrder(userID int64, item string, quantity int) error {
/*
insert data to database
_, err := r.db.Exec()
*/
return nil
}

Service is part where we put resource as dependency, and using it to do all of our business logic. In constructing this part, we will rely on interface and it will be a part that consuming the resource. So if a type (resource) support multiple functions and your service only need one of them, we can define it on the interface at the point of use.

package orderimport "errors"type (
// Order represent order table
Order struct {
ID int64 `db:"id"`
UserID int64 `db:"user_id"`
Item string `db:"item"`
Quantity int `db:"quantity"`
Amount int64 `db:"amount"`
}
// UserService indicate what function from user service that being
// used by order service
UserService interface {
IsLoggedIn(userID int64) (bool, error)
}
// OrderResource indicate what function being used from resource
// order
OrderResource interface {
CreateOrder(userID int64, item string, quantity int) error
}
// Service struct containing list of dependency from service order
Service struct {
userService UserService // This is how each service communicate with each others
orderResource OrderResource
}
)
// New construct new order service
func New(orderResource OrderResource, userService UserService) *Service {
return &Service{
orderResource: orderResource,
userService: userService,
}
}
// NewOrder is example of using multiple service
func (s *Service) NewOrder(userID int64, item string, qty int) error {
loggedIn, err := s.userService.IsLoggedIn(userID)
if err != nil {
return err
}
if !loggedIn {
return errors.New("please login")
}
return s.orderResource.CreateOrder(userID, item, qty)
}

On the code above we can see that we use userservice and put it as a dependency to orderservice . Since the userservice is an interface, we can easily make mock of that service and when we are going to test a function that using it, our test become more focus on testing the business logic.

Injector will work as a place where you instantiate all your dependencies such as database connection, service packages, resource packages, etc and do the injection to each part. I didn’t implement dependency container approach or others like uber-go/dig or google/wire but rather much simple approach by treating dependencies as parameters to type or constructor.

package mainimport (
resourceorder "github.com/nydan/resource/order/order"
resourceuser "github.com/nydan/resource/user/user"
serviceorder "github.com/nydan/service/order/order"
serviceuser "github.com/nydan/service/user/user"
)
func main() {
cfg := GetConfig()
db, err := ConnectDatabase(cfg.Database)
if err != nil {
panic(err)
}
cache, err := ConnectRedis(cfg.Redis)
if err != nil {
panic(err)
}
resourceUser := resourceuser.New(db, cache)
serviceUser := serviceuser.New(resourceUser)
resourceOrder := resourceorder.New(db)
serviceOrder := serviceorder.New(resourceOrder, serviceUser)
server := NewServer(cfg.ListenAddr, serviceUser, serviceOrder)
server.Run()
}

At the code above, by the end all the instantiated services will become a parameter on NewServer where you may put your http.Handler there. For my implementation, I made one more package for managing my API and define an interface again at the point of use for what service going to be exposed to public.

Based on my experience, this approach working well for my requirements. It help engineers to easily adapt the workflow because the code is getting clean and straight forward. It help us to create more efficient unit test because we can mock some part of a function and more focus on the business logic. It help us to easily maintain our code since if we are going to add and/or delete more dependencies, it is checkable on the compile time.

--

--

Danny Aguswahyudi
Danny Aguswahyudi

No responses yet