The Pitfalls of Directly Using Third-Party Modules in Your Golang Production Code

Chazool
5 min readSep 9, 2023

Introduction

Third-party modules have become an integral part of modern software development, offering a way to extend functionality without reinventing the wheel. In the realm of Golang development, third-party modules provide a valuable shortcut to building robust applications efficiently. They range from database drivers like GORM and logging libraries such as zap, to utility packages like viper for configuration management. However, while they offer convenience, directly integrating third-party modules into your production code might not be the best strategy. In this article, we explore the potential problems and risks of this approach and propose an alternative: utilizing custom packages or code.

The Problems

Directly incorporating third-party modules into your codebase can lead to several challenges:

Compatibility Issues: Third-party modules can evolve independently of your application, causing potential mismatches between your code and module updates. These changes might result in breaking your existing codebase or introducing subtle bugs. For instance, the transition from the `ioutil` package to the `io` and `os` packages in Go 1.16.

Stability and Security Risks: Modules, even well-maintained ones, can be deprecated or abandoned by their maintainers. This can leave your code susceptible to security vulnerabilities or performance issues. For example, the `jaeger-client` library is deprecated and will no longer receive updates, which could affect the stability and security of your application if you continue to rely on it.

Replacement: Newer and better alternatives can replace existing modules, offering enhanced features or efficiency. For instance, the introduction of the `slog` package in Go 1.21 as a replacement for the `zap` package for structured logging.

How to Fix It

golang third-party package manager

To mitigate the issues of directly using third-party modules, consider the following strategies:

Abstraction: Build custom packages that encapsulate third-party module functionality and expose a stable API for your code. This allows for easier replacement of modules without major code changes. For instance, you can create a custom package that uses GORM internally for database access but provides a simplified CRUD interface for your models.

Implementation: Craft your own code to fulfill your application’s requirements, bypassing third-party modules entirely. This grants you full control and ownership over your codebase and the ability to optimize it for your unique needs. For example, instead of relying on external logging libraries, create your own logging functions using Go’s standard `log` package.

Guidelines for creating custom packages or code include comprehensive documentation, thorough testing, proper versioning, and compliance with licensing requirements.

Sample Implementation

The code has been effectively decoupled from the specific database library (in this example, GORM). This abstraction allows us to switch to a different database library in the future by implementing functions specific to the new library within the ‘configs’ package. Whether we decide to use a different SQL database library, a NoSQL database, or any other data storage solution, we can create a new database provider within ‘configs.’ This provider can encapsulate interactions with the new database library without requiring extensive changes to the repository or other parts of the application.

By implementing this additional level of abstraction, Go production code gains greater flexibility and maintainability. It can adapt to changing requirements or take advantage of new database technologies without compromising the overall structure and functionality of the application. This approach follows the principle of separating concerns and makes the codebase more robust and adaptable in the long run

Package configs: Encapsulate the database functionality using a custom package and interface.

package configs

import (
"fmt"
"modulemanager/dto"

"gorm.io/driver/postgres"
"gorm.io/gorm"
)

var dbCon DBConnection

type DBConnection *gorm.DB

// DBConfig defines the database configuration parameters.
type DBConfig struct {
_ struct{}
Host string
User string
Password string `json:"_"`
Port string
DB string
SSLMode string
TimeZone string
}

type DBOperator struct {
Con DBConnection
}

// NewConnection initializes the database connection based on the provided config.
func (db *DBConfig) NewConnection() (DBConnection, error) {
dsn := "host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s"
dialector := postgres.Open(fmt.Sprintf(dsn, db.Host, db.User, db.Password, db.DB, db.Port, db.SSLMode, db.TimeZone))
con, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
return nil, err
}

con.AutoMigrate(&dto.User{})

return con, nil
}

// Save saves the given value to the database.
func (db *DBOperator) Save(value interface{}) error {
return db.Con.Statement.Create(value).Error
}

// Find finds and retrieves data from the database and stores it in dest.
func (db *DBOperator) Find(dest interface{}) error {
return db.Con.Statement.Find(dest).Error
}

// InitDBConnection initializes the database connection.
func InitDBConnection() {
dbConfig := DBConfig{
Host: "localhost",
User: "postgres",
Password: "postgres",
Port: "5432",
DB: "testapp",
SSLMode: "disable",
TimeZone: "Asia/Shanghai",
}
con, err := dbConfig.NewConnection()
if err != nil {
return
}
dbCon = con
}

// GetDBConnection returns the database connection.
func GetDBConnection() DBConnection {
return dbCon
}

Package main: Use the custom repository and database connection.

package main

import (
"fmt"
"modulemanager/configs"
"modulemanager/dto"
"modulemanager/repo"
)

func init() {
// init db connection
configs.InitDBConnection()
}

func main() {
users := []dto.User{
{
UserName: "user1",
Password: "test1",
},
{
UserName: "user2",
Password: "test2",
},
}

repo := repo.NewUserRepo()

// save users
err := repo.Save(users...)
if err != nil {
return
}

// get all users
users, err = repo.FindAll()
if err != nil {
return
}

// print users
for _, user := range users {
fmt.Printf("%+v\n", user)
}

}

Conclusion

Directly integrating third-party modules into your Golang production code can lead to a host of issues, including breaking changes, abandonment, and the risk of replacement. However, by embracing custom packages or code, you can enjoy numerous benefits, including increased reliability, enhanced security, improved performance, and elevated code quality. Taking the time to develop custom solutions tailored to your application’s needs ensures a smoother development process and a more sustainable codebase in the long run.

Third Party Module Manager Repository

— happy coding 😉 —

--

--