Effective Management of Custom Types in Go with GORM: Leveraging Valuer and Scanner Interface

Kalani Ishanka
Thirdfort
Published in
5 min readMay 31, 2024

At Thirdfort, our commitment to continuous improvement led us to integrate the GORM library into our development stack, which has become a pivotal part of our software architecture. This decision was motivated by our desire to simplify database interactions and improve code maintainability. By adopting GORM, our developers can focus on application logic rather than database complexities, which not only improves productivity but also strengthens our system’s security.

During a key project that involved managing complex custom types in Go, we delved into how to effectively leverage GORM for these tasks. This article will discuss the strategies and insights gained from this exploration.

Integrating GORM at Thirdfort

GORM is a powerful Object-Relational Mapping (ORM) tool that seamlessly bridges the gap between our Go application code and the underlying database. This tool remarkably simplifies database interactions, empowering developers to manage data as if they were handling simple Go objects. This eliminates the need for complex SQL query writing, making our work more efficient and productive.

Key Advantages:

  1. Automatic Migrations: With GORM, we confidently facilitate smooth updates to the database schema as our application data structures mature. This ensures our database always matches our current needs, without any manual tweaking.Moreover, this capability improves our application’s scalability by facilitating the creation of scalable data models. It allows us to smoothly adapt to changing data requirements and business logic, ensuring our system grows and changes with minimal downtime and complex migrations.
  2. Advanced Querying: GORM handles a range from simple CRUD operations to complicated queries with joins and nested transactions. This makes it effective in managing complex data relationships and enhancing database performance.
  3. Security Enhancements: GORM includes built-in safety features such as prepared statements, which enhance security by significantly reducing the risk of SQL injection attacks.

While these features make GORM an indispensable part of our development stack at Thirdfort, enhancing our productivity and application robustness, there are some potential drawbacks to consider. These include performance overhead in complex queries, a learning curve for new users, and a degree of reduced control over generated SQL. Awareness of the potential downsides and actively managing them is essential to fully leverage GORM’s capabilities without compromising on performance and flexibility.

Approaching Custom Types with GORM

GORM excels at managing basic data types by easily mapping them to database tables, eliminating the need for manual SQL script writing. This feature simplifies initial data handling, letting us concentrate more on application logic instead of database complexities. For example, take a simple User struct:

type User struct {
Name string json:"name"
Age int json:"age"
}

GORM manages this struct automatically, creating the required database tables and fields. It makes inserting and retrieving data straightforward.

// Creating a new user
user := User{Name: "Jhon", Age: 30}
db.Create(&user)
// Retrieving users
var users []User
db.Find(&users)

However, when it comes to integrating more complex custom types, the standard methods fall short. Facing the challenge of integrating custom types, we explored the use of GORM’s Valuer and Scanner interfaces to ensure seamless data serialization and deserialization within our relational databases. Let's now take a closer look at how we implemented these interfaces to effectively manage our data.

Implementing Valuer and Scanner Interfaces

Handling complex custom types in GORM can greatly enhance data management capabilities. For example, let’s consider a Address struct that includes street, city, state and ZipCode. We need to ensure this data can be easily stored and retrieved from a database, which requires implementing serialization and deserialization methods. Here’s how you can do this using the Valuer and Scanner interfaces.

// User represents a user profile with an embedded address.
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
}
// Address represents a user's address.
type Address struct {
Street string `json:"street"`
City string `json:"city"`
State string `json:"state"`
ZipCode string `json:"zipcode"`
}

Implementing Valuer Interface:

To enable GORM to properly store instances of the Address struct in your database, you'll want to implement the Valuer interface. This interface requires defining a method called Value(), which returns a driver.Value and an error. The driver.Value type can be any one of the following Go types that are compatible with the database: int64, float64, bool, []byte, string, time.Time, or nil. In our example, this method would be responsible for serializing the Address struct into a format that can be stored in the database.

func (a Address) Value() (driver.Value, error) {
// Serialize the Address struct into a format suitable for storage
// For example, you might serialize it into a JSON string
addressJSON, err := json.Marshal(a)
if err != nil {
return nil, err
}
return string(addressJSON), nil
}

Implementing Scanner Interface:

On the other hand, implementing the Scanner interface allows GORM to properly read and deserialize data from the database back into instances of the Address struct within your Go application. This interface requires defining a method called Scan(value interface{}), which accepts a value from the database and updates the receiver with the deserialized data.

func (a *Address) Scan(value interface{}) error {
// Deserialize the value from the database into the Address struct
// For example, if the value is stored as a JSON string, you would unmarshal it
addressJSON, ok := value.(string)
if !ok {
return errors.New("unexpected type for address")
}
return json.Unmarshal([]byte(addressJSON), a)
}

By implementing these interfaces for your custom types, you empower GORM to seamlessly handle the serialization and deserialization of complex data structures, ensuring your data remains intact as it moves between your Go application and your relational database. One of Go’s standout features is its ability to satisfy interfaces implicitly, which streamlines implementations such as this one. With the feature of implicit satisfaction of interfaces, types that implement the required methods don’t need explicit declarations. This makes the implementation of custom behaviors like serialization and deserialization with GORM simpler.

Conclusion

Integrating GORM’s capabilities for managing custom types not only enhances the flexibility and scalability of your applications but also simplifies the process of working with complex data structures in a relational database environment. By leveraging the Valuer and Scanner interfaces, you can ensure that your data remains consistent and easily accessible throughout your application's lifecycle.

--

--