Skip to main content
  1. Articles/

Golang Tutorial: Generics with Gorm

·1741 words·9 mins· loading · loading ·
Marko Milojevic
Author
Marko Milojevic
Software engineer and architect. Golang and LLM enthusiast. Awful chess player, gym rat, harmonica newbie and cat lover.
Generics in Golang - This article is part of a series.
Part 2: This Article

After months and years of talking, trying things out, and testing, we’ve finally reached a big moment in our favorite programming language. The new Golang version, 1.18, is here. We knew it would bring significant changes to Go’s codebase, even before Generics was officially released. For a long time, when we wanted to make our code more general and abstract, we used code generators in Go. Learning what the “Go way” of doing things was challenging for many of us, but it also led to many breakthroughs. It was worth the effort. Now, there are new possibilities on the horizon.

Many new packages have emerged, giving us ideas on how we can improve the Go ecosystem with reusable code that makes life easier for all of us. This inspiration led me to create a small proof of concept using the Gorm library. Now, let’s take a look at it.

Source code
#

When I wrote this article, it relied on a GitHub Repository. The code served as a Go library proof of concept, with my intention to continue working on it. However, it was not yet suitable for production use, and I had no plans to offer production support at that time.

You can find the current features by following the link, and below, there is a smaller sample snippet.

Example Usage

package main

import (
  	"github.com/ompluscator/gorm-generics"
	// some imports
)

// Product is a domain entity
type Product struct {
	// some fields
}

// ProductGorm is DTO used to map Product entity to database
type ProductGorm struct {
	// some fields
}

// ToEntity respects the gorm_generics.GormModel interface
func (g ProductGorm) ToEntity() Product {
	return Product{
		// some fields
	}
}

// FromEntity respects the gorm_generics.GormModel interface
func (g ProductGorm) FromEntity(product Product) interface{} {
	return ProductGorm{
		// some fields
	}
}

func main() {
	db, err := gorm.Open(/* DB connection string */)
	// handle error

	err = db.AutoMigrate(ProductGorm{})
	// handle error

	// initialize a new Repository with by providing
	// GORM model and Entity as type
	repository := gorm_generics.NewRepository[ProductGorm, Product](db)

	ctx := context.Background()

	// create new Entity
	product := Product{
		// some fields
	}
	
	// send new Entity to Repository for storing
	err = repository.Insert(ctx, &product)
	// handle error

	fmt.Println(product)
	// Out:
	// {1 product1 100 true}

	single, err := repository.FindByID(ctx, product.ID)
	// handle error
	
	fmt.Println(single)
	// Out:
	// {1 product1 100 true}
}

Why have I picked ORM for PoC?
#

Coming from a background in software development with traditional object-oriented programming languages like Java, C#, and PHP, one of the first things I did was search Google for a suitable ORM for Golang. Please forgive my inexperience at the time, but that’s what I was expecting. It’s not that I can’t work without an ORM. It’s just that I don’t particularly like how raw MySQL queries appear in the code. All that string concatenation looks messy to me. On the other hand, I always prefer to dive right into writing business logic, with minimal time spent thinking about the underlying data storage. Sometimes, during the implementation, I change my mind and switch to different types of storage. That’s where ORMs come in handy.

In summary, ORM provides me with:

  1. Cleaner code.
  2. More flexibility in choosing the type of underlying data storage.
  3. The ability to focus entirely on business logic rather than technical details.

There are many ORM [solutions](https://github.com/d-tsuji/awesome-go-orms solutions) available for Golang, and I’ve used most of them. Not surprisingly, I’ve used GORM the most because it covers a wide range of features. Yes, it lacks some well-known patterns like Identity Map, Unit of Work, and Lazy Load, but I can work without them. However, I have often missed the Repository pattern because I’ve encountered duplicated or very similar code blocks from time to time (and I really dislike repeating myself).

For that purpose, I sometimes used the GNORM library, which had templating logic that allowed me to create Repository structures with freedom. While I liked the idea that GNORM presented (very much in line with The Golang Way!), constantly updating templates to add new features to the Repository didn’t look good. I attempted to provide my own implementation that relied on reflection and share it with the Open Source community. Unfortunately, it didn’t go as planned. It worked, but maintaining the library was painful, and its performance was not exceptional. In the end, I deleted the GitHub repository. And just as I was giving up on this ORM upgrade in Go, Generics came into play. Oh, boy. Oh, boy! I was back to the drawing board immediately.

Implementation
#

Part of my background involves Domain-Driven Design. This means I prefer to separate the domain layer from the infrastructure layer. Some ORMs treat the Entity pattern more like a Row Data Gateway or Active Record. However, because its name references the DDD pattern Entity, we can sometimes get confused and end up mixing business logic and technical details in the same class, creating a kind of monster.

The Entity pattern isn’t related to mapping to a database table schema or the underlying storage in any way. So, I always use Entity in the domain layer and Data Access Objects (DAO in the infrastructure layer. The signature of my Repositories always supports only Entity, but internally, they use DTO to map data to and from a database and fetch and store them into Entity. This approach guarantees a functional Anti-Corruption Layer.

In this case, I work with a trio of interfaces and structures, as you can see in the diagram below:

  1. Entity, which holds business logic in the domain layer.
  2. GormModel, serving as a DAO used to map data from Entity into a database.
  3. GormRepository, functioning as an orchestrator for querying and persisting data.
Gorm Generics
Gorm Generics

Two main parts, GormModel and GormRepository, require generic types to define the signatures of their methods. Utilizing generics enables us to specify GormRepository as a struct and create a more generalized implementation:

GormRepository methods

func (r *GormRepository[M, E]) Insert(ctx context.Context, entity *E) error {
  	// map the data from Entity to DTO
	var start M
	model := start.FromEntity(*entity).(M)

  	// create new record in the database
	err := r.db.WithContext(ctx).Create(&model).Error
	// handle error

  	// map fresh record's data into Entity
	*entity = model.ToEntity()
	return nil
}

func (r *GormRepository[M, E]) FindByID(ctx context.Context, id uint) (E, error) {
  	// retrieve a record by id from a database
	var model M
	err := r.db.WithContext(ctx).First(&model, id).Error
	// handle error

  	// map data into Entity
	return model.ToEntity(), nil
}

func (r *GormRepository[M, E]) Find(ctx context.Context, specification Specification) ([]E, error) {
  	// retreive reords by some criteria
	var models []M
	err := r.db.WithContext(ctx).Where(specification.GetQuery(), specification.GetValues()...).Find(&models).Error
	// handle error

  	// mapp all records into Entities
	result := make([]E, 0, len(models))
	for _, row := range models {
		result = append(result, row.ToEntity())
	}

	return result, nil
}

I didn’t intend to add more or less complex features like preloading, joins, or even limit and offset for this proof of concept. The idea was to test the simplicity of implementing generics in Go with the GORM library. In the code snippet, you can see that the GormRepository struct supports inserting new records, retrieving records by identity, and querying by Specification.

The Specification pattern is another pattern from Domain-Driven Design that we can use for various purposes, including querying data from storage. The proof of concept provided here defines a Specification interface, which provides a WHERE clause and the values used inside it. This does require some usage of generics for comparable operators and could potentially serve as a precursor for a future Query Object:

Specification example

type Specification interface {
	GetQuery() string
	GetValues() []any
}

// joinSpecification is the real implementation of Specification interface.
// It is used fo AND and OR operators.
type joinSpecification struct {
	specifications []Specification
	separator      string
}

// GetQuery concats all subqueries
func (s joinSpecification) GetQuery() string {
	queries := make([]string, 0, len(s.specifications))

	for _, spec := range s.specifications {
		queries = append(queries, spec.GetQuery())
	}

	return strings.Join(queries, fmt.Sprintf(" %s ", s.separator))
}

// GetQuery concats all subvalues
func (s joinSpecification) GetValues() []any {
	values := make([]any, 0)

	for _, spec := range s.specifications {
		values = append(values, spec.GetValues()...)
	}

	return values
}

// And delivers AND operator as Specification
func And(specifications ...Specification) Specification {
	return joinSpecification{
		specifications: specifications,
		separator:      "AND",
	}
}

// notSpecification negates sub-Specification
type notSpecification struct {
	Specification
}

// GetQuery negates subquery
func (s notSpecification) GetQuery() string {
	return fmt.Sprintf(" NOT (%s)", s.Specification.GetQuery())
}

// Not delivers NOT operator as Specification
func Not(specification Specification) Specification {
	return notSpecification{
		specification,
	}
}

// binaryOperatorSpecification defines binary operator as Specification
// It is used for =, >, <, >=, <= operators.
type binaryOperatorSpecification[T any] struct {
	field    string
	operator string
	value    T
}

// GetQuery builds query for binary operator
func (s binaryOperatorSpecification[T]) GetQuery() string {
	return fmt.Sprintf("%s %s ?", s.field, s.operator)
}

// GetValues returns a value for binary operator
func (s binaryOperatorSpecification[T]) GetValues() []any {
	return []any{s.value}
}

// Not delivers = operator as Specification
func Equal[T any](field string, value T) Specification {
	return binaryOperatorSpecification[T]{
		field:    field,
		operator: "=",
		value:    value,
	}
}

The Specification part of the package offers the ability to provide custom criteria to the Repository and fetch data that meets those criteria. It allows for combining, negating, and extending criteria as needed.

Results
#

This implementation ultimately achieves the main objective of this proof of concept, which is to create a generalized interface for querying records from the database.

Outcome

err := repository.Insert(ctx, &Product{
    Name:        "product2",
    Weight:      50,
    IsAvailable: true,
})
// error handling

err = repository.Insert(ctx, &Product{
    Name:        "product3",
    Weight:      250,
    IsAvailable: false,
})
// error handling

many, err := repository.Find(ctx, gorm_generics.And(
    gorm_generics.GreaterOrEqual("weight", 90),
    gorm_generics.Equal("is_available", true)),
)
// error handling

fmt.Println(many)
// Out:
// [{1 product1 100 true}]

Concerning my aspirations, the code snippet from above delivers a quick and elegant way to retrieve data in a clean and readable form. And without affecting performance (significantly).

Conclusion
#

Exploring generics for the first time following the official release of Go 1.18 was quite refreshing. I’ve been facing some challenges lately, and having this opportunity for new ideas was just what I needed. Additionally, resuming my blogging after a long break was something I felt compelled to do. It’s wonderful to share my opinions publicly once more, and I’m eagerly anticipating all the feedback you folks can provide.

Useful Resources
#

Generics in Golang - This article is part of a series.
Part 2: This Article

Related

Golang Tutorial: Generics

·3344 words·16 mins· loading · loading
How often do we encounter significant changes in our preferred programming language? Some languages undergo frequent updates, while others remain traditional and stable. Go falls into the latter category, known for its consistency. “This is not the Go way!” is a phrase that often comes to mind. Most Go releases have focused on refining its existing principles. However, a major shift is on the horizon. The Go team has announced that Generics in Go are becoming a reality, moving beyond mere discussion and into implementation. Brace yourselves, a revolution is coming. What are Generics? # Generics allow us to parameterize types when defining interfaces, functions, and structs. Generics is not a new concept. It has been used since the first version of Ada, through templates in C++, to its modern implementations in Java and C#. To illustrate without delving into complex definitions, let’s examine a practical example. Instead of having multiple Max or Min functions like this: Without Generics func MaxInt(a, b int) int { // some code } func MaxFloat64(a, b float64) float64 { // some code } func MaxByte(a, b byte) byte { // some code } we can declare now only one method, like this: With Generics func Max[T constraints.Ordered](a, b T) T { // some code } Wait, what just happened? Instead of defining a method for each type in Go, we utilized Generics. We used a generic type, parameter T, as an argument for the method. With this minor adjustment, we can support all orderable types. The parameter T can represent any type that satisfies the Ordered constraint (we will discuss constraints later). Initially, we need to specify what kind of type T is. Next, we determine where we want to use this parameterized type. In this case, we’ve specified that both input arguments and the output should be of type T. If we execute the method by defining T as an integer, then everything here will be an integer: Execute Generic Function func main() { fmt.Println(Max[int](1, 2)) } // // this code behaves exactly like method: // Max(a, b int) int And it doesn’t stop there. We can provide as many parameterized types as we need and assign them to different input and output arguments as desired: Execute some complex Generic Function func Do[R any, S any, T any](a R, b S) T { // some code } func main() { fmt.Println(Do[int, uint, float64](1, 2)) } // // this code behaves exactly like method: // Do(a int, b uint) float64 Here we have three parameters: R, S, and T. As we can see from the any constraint (which behaves like interface{}), those types can be, well, anything. So, up to this point, we should have a clear understanding of what generics are and how we use them in Go. Let’s now focus on more exciting consequences.

Golang Tutorial: Unit Testing with Mocking

·3513 words·17 mins· loading · loading
Unit testing has always been my thing, almost like a hobby. There was a time when I was obsessed with it, and I made sure that all my projects had at least 90% unit test coverage. You can probably imagine how much time it can take to make such a significant change in the codebase. However, the result was worth it because I rarely encountered bugs related to business logic. Most of the issues were related to integration problems with other services or databases. Adding new business rules was a breeze because there were already tests in place to cover all the cases from before. The key was to ensure that these tests remained successful in the end. Sometimes, I didn’t even need to check the entire running service; having the new and old unit tests pass was sufficient. Once, while working on a personal project, I had to write unit tests to cover numerous Go structs and functions—more than 100 in total. It consumed my entire weekend, and late on a Sunday night, before heading out on a business trip the next day, I set an alarm clock to wake me up. I had hardly slept that night; it was one of those restless nights when you dream but are also aware of yourself and your surroundings. My brain was active the entire time, and in my dreams, I kept writing unit tests for my alarm clock. To my surprise, each time I executed a unit test in my dream, the alarm rang. It continued ringing throughout the night. And yes, I almost forgot to mention, for two years, we had zero bugs in production. The application continued to fetch all the data and send all the emails every Monday. I don’t even remember my Gitlab password anymore. Unit Testing and Mocking (in general) # In Martin Fowler’s article, we can identify two types of unit tests: Sociable unit tests, where we test a unit while it relies on other objects in conjunction with it. For example, if we want to test the UserController, we would test it along with the UserRepository, which communicates with the database. Solitary unit tests, where we test a unit in complete isolation. In this scenario, we would test the UserController, which interacts with a controlled, mocked UserRepository. With mocking, we can specify how it behaves without involving a database.

Practical SOLID in Golang: Interface Segregation Principle

·1722 words·9 mins· loading · loading
When beginners embark on their programming journey, the initial focus is typically on algorithms and adapting to a new way of thinking. After some time, they delve into Object-Oriented Programming (OOP). If this transition is delayed, it can be challenging to shift from a functional programming mindset. However, eventually, they embrace the use of objects and incorporate them into their code where necessary, sometimes even where they’re not needed. As they learn about abstractions and strive to make their code more reusable, they may overgeneralize, resulting in abstractions applied everywhere, which can hinder future development. At some point, they come to realize the importance of setting boundaries for excessive generalization. Fortunately, The Interface Segregation Principle has already provided a guideline for this, representing the “I” in the word SOLID. When we do not respect The Interface Segregation # Maintain small interfaces to prevent users from relying on unnecessary features. Uncle Bob introduced this principle, and you can find more details about it on his blog. This principle clearly states its requirement, perhaps better than any other SOLID principle. Its straightforward advice to keep interfaces as small as possible should not be interpreted as merely advocating one-method interfaces. Instead, we should consider the cohesion of features that an interface encompasses. Let’s analyze the code below: User interface type User interface { AddToShoppingCart(product Product) IsLoggedIn() bool Pay(money Money) error HasPremium() bool HasDiscountFor(product Product) bool // // some additional methods // } Let’s assume we want to create an application for shopping. One approach is to define an interface User, as demonstrated in the code example. This interface includes various features that a user can have. On our platform, a User can add a Product to the ShoppingCart, make a purchase, and receive discounts on specific Products. However, the challenge is that only specific types of Users can perform all of these actions. Guest struct type Guest struct { cart ShoppingCart // // some additional fields // } func (g *Guest) AddToShoppingCart(product Product) { g.cart.Add(product) } func (g *Guest) IsLoggedIn() bool { return false } func (g *Guest) Pay(Money) error { return errors.New("user is not logged in") } func (g *Guest) HasPremium() bool { return false } func (g *Guest) HasDiscountFor(Product) bool { return false } We have implemented this interface with three structs. The first one is the Guest struct, representing a user who is not logged in but can still add a Product to the ShoppingCart. The second implementation is the NormalCustomer, which can do everything a Guest can, plus make a purchase. The third implementation is the PremiumCustomer, which can use all features of our system.