One of the most powerful features in Go is its interface system. Unlike many other languages, Go's interfaces are implicit, lightweight, and incredibly effective at decoupling your application components. In this article, we'll explore how interfaces can transform a tightly-coupled codebase into a flexible, maintainable architecture.
Let's start with a common scenario. You're building a user management system with PostgreSQL:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
type User struct {
ID int
Name string
}
type UserService struct {
postgresDB *sql.DB // Directly depends on PostgreSQL
}
func NewUserService(db *sql.DB) *UserService {
return &UserService{postgresDB: db}
}
func (s *UserService) RegisterUser(user User) error {
_, err := s.postgresDB.Exec(
"INSERT INTO users (name) VALUES ($1)", // PostgreSQL syntax
user.Name,
)
return err
}
func (s *UserService) ListUsers() ([]User, error) {
rows, err := s.postgresDB.Query("SELECT id, name FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
rows.Scan(&u.ID, &u.Name)
users = append(users, u)
}
return users, nil
}This code has several critical issues:
- Database Lock-in: Your business logic (UserService) is tightly coupled to PostgreSQL
- Testing Nightmare: You need a real database to test your business logic
- No Flexibility: Switching to MySQL, MongoDB, or even a mock implementation requires rewriting UserService
- Violation of SOLID Principles: Specifically, the Dependency Inversion Principle
Let's refactor this using Go interfaces to achieve decoupling.
First, we define what operations we need, not how they're implemented:
// repository/repository.go
package repository
import "project/models"
// UserRepository defines the contract for user data access
type UserRepository interface {
Create(user models.User) error
GetAll() ([]models.User, error)
}Key Insight: This interface represents a contract. Any type that implements these methods satisfies the interface, no explicit declaration needed.
// repository/postgres.go
package repository
import (
"database/sql"
"fmt"
"project/models"
)
type PostgresRepo struct {
db *sql.DB
}
func NewPostgresRepo(db *sql.DB) *PostgresRepo {
return &PostgresRepo{db: db}
}
func (p *PostgresRepo) Create(user models.User) error {
res, err := p.db.Exec(
"INSERT INTO users (name) VALUES ($1)", // PostgreSQL-specific
user.Name,
)
if err != nil {
return fmt.Errorf("failed to insert user: %w", err)
}
rows, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
fmt.Println("Inserted rows:", rows)
return nil
}
func (p *PostgresRepo) GetAll() ([]models.User, error) {
rows, err := p.db.Query("SELECT id, name FROM users")
if err != nil {
return nil, fmt.Errorf("failed to query users: %w", err)
}
defer rows.Close()
var users []models.User
for rows.Next() {
var u models.User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, fmt.Errorf("failed to scan user: %w", err)
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating rows: %w", err)
}
return users, nil
}Here's where the magic happens , we can add MySQL support without touching existing code:
// repository/mysql.go
package repository
import (
"database/sql"
"fmt"
"project/models"
)
type MySQLRepo struct {
db *sql.DB
}
func NewMySQLRepo(db *sql.DB) *MySQLRepo {
return &MySQLRepo{db: db}
}
func (m *MySQLRepo) Create(user models.User) error {
_, err := m.db.Exec(
"INSERT INTO users (name) VALUES (?)", // MySQL syntax (different!)
user.Name,
)
if err != nil {
return fmt.Errorf("failed to insert user: %w", err)
}
return nil
}
func (m *MySQLRepo) GetAll() ([]models.User, error) {
rows, err := m.db.Query("SELECT id, name FROM users")
if err != nil {
return nil, fmt.Errorf("failed to query users: %w", err)
}
defer rows.Close()
var users []models.User
for rows.Next() {
var u models.User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, fmt.Errorf("failed to scan user: %w", err)
}
users = append(users, u)
}
return users, nil
}Notice: Both PostgresRepo and MySQLRepo satisfy the UserRepository interface without explicitly declaring it. This is Go's implicit interface satisfaction.
Now our service layer depends on the interface, not concrete implementations:
// service/user_service.go
package service
import (
"fmt"
"project/models"
"project/repository"
)
type UserService struct {
repo repository.UserRepository // Depends on interface, not implementation!
}
func NewUserService(repo repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) RegisterUser(name string) error {
// Business logic validation
if name == "" {
return fmt.Errorf("user name cannot be empty")
}
user := models.User{Name: name}
if err := s.repo.Create(user); err != nil {
return fmt.Errorf("failed to register user: %w", err)
}
return nil
}
func (s *UserService) ListUsers() ([]models.User, error) {
users, err := s.repo.GetAll()
if err != nil {
return nil, fmt.Errorf("failed to list users: %w", err)
}
return users, nil
}Critical Point: UserService has zero knowledge about PostgreSQL, MySQL, or any specific database. It only knows about the UserRepository interface.
In your main function, switching databases is trivial:
// main.go
package main
import (
"log"
"project/config"
"project/repository"
"project/service"
)
func main() {
// Option 1: Use PostgreSQL
pgConfig := config.DatabaseConfig{
Host: "localhost",
Port: 5433,
User: "postgres",
Password: "postgres",
DBName: "appdb",
SSLMode: "disable",
}
db, err := config.NewPostgresConnection(pgConfig)
if err != nil {
log.Fatal(err)
}
defer db.Close()
repo := repository.NewPostgresRepo(db)
// Option 2: Switch to MySQL - just change these two lines!
// db, err := config.NewMySQLConnection(mysqlConfig)
// repo := repository.NewMySQLRepo(db)
// The rest of the code remains identical
userService := service.NewUserService(repo)
users, _ := userService.ListUsers()
fmt.Println("Users:", users)
}