编写干净且可维护的代码是软件开发的关键方面。干净代码的关键原则之一是接受接口并返回结构。本文探讨了如何在 Go 中实现这一原则,以 UserRepository 为例。
通过接受接口和返回结构,我们在 Go 代码中实现了多个好处。遵循此原则使 Go 代码更加整洁、可维护和灵活:
1. 解耦:实现可以轻松替换,而不影响代码的其他部分。
2. 可测试性:模拟 UserRepository 接口简化了测试。
3. 可扩展性:添加新的实现或修改现有实现变得更容易管理。
4. Go 中接口的威力:在 Go 中,接口提供了一种定义类型必须实现的一组方法的方式,而无需指定这些方法的实现方式。这允许更灵活和模块化的代码,使更换实现、测试代码和扩展功能变得更容易。
5. 代码重用性和可互换性:接受接口并返回结构被认为是干净和惯用的 Go 代码的关键原因之一,是因为它促进了代码的重用性和可互换性。当函数或方法接受接口时,它不与特定实现绑定。这允许开发者在不更改使用接口的代码的情况下替换一个实现。在从一个数据库系统迁移到另一个或重构代码以提高性能时,这尤其有用。
6. 更简单的测试和模拟:接受接口使测试和模拟代码变得更容易。通过接受接口而不是具体类型,函数或方法在测试期间可以使用模拟实现。这允许开发者隔离正在测试的代码,并控制依赖项的行为,从而导致更准确和可靠的测试。
7. 封装实现细节:接受接口并返回结构的另一个优点是,它有助于封装实现细节。当函数或方法接受一个接口时,它关注它所需的行为,而不是提供该行为的特定类型。这种抽象有助于分离关注点,使代码更加模块化和可维护。
8. 多态性和组合:Go 中的接口支持多态性,允许基于它们实现的方法将不同的类型视为相同。这促进了组合优于继承,因为可以将不同的类型组合在一起以实现所需的功能。通过接受接口,函数和方法可以与实现所需方法的任何类型一起工作,从而轻松组合新类型或扩展现有类型。
9. 组件之间的清晰合同:通过定义和接受接口,在组件之间建立了清晰的合同。此合同指定了方法及其签名,确保组件遵循特定的行为。这有助于创建更稳健和可维护的代码库,因为对一个组件的更改不会意外地破坏依赖于相同接口的另一个组件。
定义 UserRepository 接口
第一步是创建一个 UserRepository 接口,该接口定义了任何具体实现所期望的方法。在这个例子中,我们有一个简单的 User
结构和一个 GetUserByID
方法。
package repository
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type UserRepository interface {
GetUserByID(id int64) (*User, error)
}
使用 MongoDB 实现 UserRepository 接口
为了为 MongoDB 后端实现 UserRepository 接口,我们创建了一个 MongoUserRepository
结构体,它嵌入了 *mongo.Collection
并实现了 GetUserByID
方法。
package mongo_repo
import (
"accept-interfaces/repository"
"context"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type MongoUserRepository struct {
collection *mongo.Collection
}
func (r *MongoUserRepository) GetUserByID(id int64) (*repository.User, error) {
filter := bson.M{"id": id}
user := &repository.User{}
err := r.collection.FindOne(context.Background(), filter).Decode(user)
if err != nil {
return nil, err
}
return user, nil
}
func NewMongoUserRepository(collection *mongo.Collection) *MongoUserRepository {
return &MongoUserRepository{collection: collection}
}
使用 PostgreSQL 实现 UserRepository 接口
同样地,对于 PostgreSQL 后端,我们创建了一个 PgxUserRepository
结构体,它嵌入了 *pgx.Conn
并实现了 GetUserByID
方法。
package postgres_repo
import (
"accept-interfaces/repository"
"context"
"github.com/jackc/pgx/v4"
)
type PgxUserRepository struct {
conn *pgx.Conn
}
func (r *PgxUserRepository) GetUserByID(id int64) (*repository.User, error) {
query := "SELECT id, name, email FROM users WHERE id = $1"
user := &repository.User{}
err := r.conn.QueryRow(context.Background(), query, id).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return user, nil
}
func NewPgxUserRepository(conn *pgx.Conn) *PgxUserRepository {
return &PgxUserRepository{conn: conn}
}
在 main 中展示
在下面提供的 main
包中,我们通过实现 UserRepository 接口,将我们为 PostgreSQL 和 MongoDB 仓库创建的组件组合在一起。其工作原理如下:
1. 初始化 PostgreSQL 连接:我们首先使用带有包含所需数据库连接详细信息的 DSN 字符串的 pgx.Connect
函数创建到 PostgreSQL 数据库的连接。
2. 为 PostgreSQL 创建一个 UserRepository 实例:我们通过调用 postgres_repo.NewPgxUserRepository
函数并传递 PostgreSQL 连接作为参数来实例化一个新的 PgxUserRepository
。
3. 初始化 MongoDB 连接:我们使用带有包含所需 MongoDB 连接详细信息的 URI 字符串的 mongo.Connect
函数建立到 MongoDB 服务器的连接。
4. 为 MongoDB 创建一个 UserRepository 实例:我们通过调用 mongo_repo.NewMongoUserRepository
函数并传递 MongoDB 集合作为参数来实例化一个新的 MongoUserRepository
。
5. 从选定的数据库中获取一个用户:我们定义一个 userID
变量,值为 1
,并调用 getUserFromDatabase
函数两次,每个 UserRepository 实例 (PostgreSQL 和 MongoDB) 调用一次。该函数根据传递给它的 UserRepository 实例的类型来确定使用哪个仓库。
6. 显示获取的用户:从每个数据库获取用户后,我们将用户详细信息打印到控制台。
getUserFromDatabase
函数负责根据传递给它的 UserRepository 实例选择正确的仓库实现。如果实例是 *postgres_repo.PgxUserRepository
类型,它将在 PostgreSQL 仓库上调用 GetUserByID
方法。如果实例是 *mongo_repo.MongoUserRepository
类型,它将在 MongoDB 仓库上调用 GetUserByID
方法。
通过遵循这种方法,我们可以创建干净、模块化且易于维护的代码,同时遵循 UserRepository 接口的契约,与不同类型的数据库一起工作。
package main
import (
"context"
"fmt"
"github.com/jackc/pgx/v4"
"log"
"time"
"accept-interfaces/repository"
"accept-interfaces/repository/mongo_repo"
"accept-interfaces/repository/postgres_repo"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
time.Sleep(10 * time.Second)
// Create a UserRepository instance for PostgreSQL
dsn := fmt.Sprintf("host=localhost port=5432 user=postgres password=postgres dbname=db sslmode=disable")
pgConn, err := pgx.Connect(context.Background(), dsn)
if err != nil {
log.Fatalf("Unable to connect to PostgreSQL: %v", err)
}
pgUserRepo := postgres_repo.NewPgxUserRepository(pgConn)
// Create a UserRepository instance for MongoDB
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI("mongodb://root:pass@localhost:27017/"))
if err != nil {
log.Fatalf("Unable to connect to MongoDB: %v", err)
}
mongoUserRepo := mongo_repo.NewMongoUserRepository(client.Database("test").Collection("users"))
// Fetch a user from the selected database
userID := int64(1)
user, err := getUserFromDatabase(pgUserRepo, userID)
if err != nil {
log.Fatalf("Error fetching user: %v", err)
}
fmt.Printf("User from PostgreSQL: %+v\n", user)
user, err = getUserFromDatabase(mongoUserRepo, userID)
if err != nil {
log.Fatalf("Error fetching user: %v", err)
}
fmt.Printf("User from MongoDB: %+v\n", user)
}
func getUserFromDatabase(repo repository.UserRepository, id int64) (*repository.User, error) {
switch r := repo.(type) {
case *postgres_repo.PgxUserRepository:
return r.GetUserByID(id)
case *mongo_repo.MongoUserRepository:
return r.GetUserByID(id)
default:
return nil, fmt.Errorf("unsupported repository type")
}
}
我用于测试的 docker-compose 文件。
version: "3.9"
services:
postgres:
image: postgres:14-alpine
container_name: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: db
volumes:
- ./postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
mongodb:
image: mongo:5.0.6-focal
container_name: mongodb
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: pass
MONGO_INITDB_DATABASE: db
volumes:
- ./mongo-data:/data/db
ports:
- "27017:27017"
在Go中接受接口并返回结构体是写出清晰、惯用代码的关键原则。它鼓励代码的可复用性、互换性、封装性和组合性,同时使测试和模拟变得更加容易。通过充分利用Go中的接口,开发者可以创建更易维护、灵活和健壮的代码库。
联系客服