Generics in Golang: refactoring example

Generics in Golang: refactoring example
Generics is a way of writing code that does not depend on the specific types used. Functions and types can now be written for any set of types.
With generics, three important functionalities are added to the language:
  • Types as parameters for functions and types.
  • Definition of interface types as type sets, including types without methods.
  • Type derivation, where in many cases types of arguments are omitted when a function is called.
This is a very useful tool that can be used in code refactoring.
I'm not going to dwell on the theoretical aspects of generics in this article, but I'll show you one example of refactoring that I applied using generics. The example is my EasyList project.
In this project, I used an interesting aspect in generics that I think can add true value when we have a whole set of methods with the same behavior.
 
 
type Folder struct {
ID int64 `jsonapi:"primary,folders"`
Name string `jsonapi:"attr,name"`
Icon string `jsonapi:"attr,icon"`
Version int32 `json:"-"`
Order int32 `jsonapi:"attr,order"`
UserId sql.NullInt64 `json:"-"`
CreatedAt time.Time `jsonapi:"attr,created_at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated_at,iso8601"`
Lists Lists `jsonapi:"relation,lists,omitempty"`
}
 
type Item struct {
ID int64 `jsonapi:"primary,items"`
UserId int64 `json:"-"`
ListId int64 `jsonapi:"attr,list_id"`
Name string `jsonapi:"attr,name"`
Description string `jsonapi:"attr,description"`
Quantity int32 `jsonapi:"attr,quantity"`
QuantityType string `jsonapi:"attr,quantity_type"`
Price float32 `jsonapi:"attr,price"`
IsStarred bool `jsonapi:"attr,is_starred"`
IsDone bool `jsonapi:"attr,is_done"`
File string `jsonapi:"attr,file"`
Order int32 `jsonapi:"attr,order"`
Version int32 `json:"-"`
CreatedAt time.Time `jsonapi:"attr,created_at,iso8601" json:"created_at" time_format:"sql_datetime"`
UpdatedAt time.Time `jsonapi:"attr,updated_at,iso8601" json:"updated_at" time_format:"sql_datetime"`
List *List `jsonapi:"relation,list,omitempty"`
}
 
Here are two specific types that represent the data needed to insert into the database. I have written a separate function for each type that performs the insertion operation in the database.
 
func (i ItemModel) Insert(item *Item) error {
var query = "INSERT INTO items (user_id, list_id, name, description, quantity, quantity_type, price, is_starred, file, version, `order`, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, NOW(), NOW())"

lastOrder, err := i.GetLastItemOrderForUser(item.UserId, item.ListId)
if err != nil {
return err
}

var args = []any{item.UserId, item.ListId, item.Name, item.Description, item.Quantity, item.QuantityType, item.Price, item.IsStarred, item.File, lastOrder}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := i.DB.ExecContext(ctx, query, args...)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
item.ID = id
item.Order = int32(lastOrder)
return nil
}
 
func (f FolderModel) Insert(folder *Folder) error {
var query = "INSERT INTO folders (user_id, name, icon, version, `order`, created_at, updated_at) VALUES (?, ?, ?, ?, ?, NOW(), NOW())"
 
lastOrder, err := f.GetLastFolderOrderForUser(folder.UserId.Int64)
if err != nil {
return err
}
 
var args = []any{folder.UserId, folder.Name, folder.Icon, folder.Version, lastOrder}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := f.DB.ExecContext(ctx, query, args...)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
folder.ID = id
folder.Order = int32(lastOrder)
return nil
}
 
If we look at the above example more closely, we see only a small difference between the two implementations. They all do the same operation-insert data into the database, get the record id. If the id assignment could be translated into generics, I could put the repeating block of code in a separate function.

Then we could create the following type:

 
type entities interface {
Folder | Item
}
 
But due to limitations of the language, we cannot assign new data to the properties, so we will declare another interface:
type Entities interface {
SetId(id int64)
}
 
And then do the implementation in both structures:
 
func (f *Folder) SetId(id int64) {
f.ID = id
}
 
Now we can write a new function that will save the data to the database:
 
func Insert[T Entities](db *sql.DB, entity T, query string, args ...any) (T, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := db.ExecContext(ctx, query, args...)
if err != nil {
return entity, err
}
id, err := result.LastInsertId()
if err != nil {
return entity, err
}
entity.SetId(id)

return entity, nil
}
 
Our method of saving a folder is now much easier:
 
func (f FolderModel) Insert(folder *Folder) error {
var query = "INSERT INTO folders (user_id, name, icon, version, `order`, created_at, updated_at) VALUES (?, ?, ?, ?, ?, NOW(), NOW())"

lastOrder, err := f.GetLastFolderOrderForUser(folder.UserId.Int64)
if err != nil {
return err
}

folder, err = Insert[*Folder](f.DB, folder, query, folder.UserId, folder.Name, folder.Icon, folder.Version, lastOrder)
if err != nil {
return err
}
folder.Order = int32(lastOrder)
return nil
}
 

The solution would be much simpler and cleaner if we didn't use setters, but assigned the id directly as follows:
entity.ID = id


But go doesn't allow us to do that yet, so we use a workaround.


Thus, generics give a certain lightness to the programming language, allow us to simplify refactoring and write functions with behavior depending on the parameters we use.

Popular Posts

My most popular posts

Maximum productivity on remote job
Business

Maximum productivity on remote job

I started my own business and intentionally did my best to work from anywhere in the world. Sometimes I sit with my office with a large 27-inch monitor in my apartment in Cheboksary. Sometimes I’m in the office or in some cafe in another city.

Hello! I am Sergey Emelyanov and I am hardworker
Business PHP

Hello! I am Sergey Emelyanov and I am hardworker

I am a programmer. I am an entrepreneur in my heart. I started making money from the age of 11, in the harsh 90s, handing over glassware to a local store and exchanging it for sweets. I earned so much that was enough for various snacks.

Hire Professional CRM developer for $25 per hour

I will make time for your project. Knowledge of Vtiger CRM, SuiteCRM, Laravel, and Vue.js. I offer cooperation options that will help you take advantage of external experience, optimize costs and reduce risks. Full transparency of all stages of work and accounting for time costs. Pay only development working hours after accepting the task. Accept PayPal and Payoneer payment systems. How to hire professional developer? Just fill in the form

Telegram
@sergeyem
Telephone
+4915211100235