Go使用Repository模式时如何处理数据库连接与类型抽象
Go 仓储模式跨数据库解耦实现方案
首先修正现有代码的一个基础问题:Go中标识符首字母小写为包私有,你定义的read方法无法在包外访问,也无法满足跨包定义的接口契约,所有需要对外暴露的接口方法、仓储实现方法都要改为大写开头(比如Read)。
1. 统一ID类型,消除数据库类型差异
不要让业务层感知不同数据库的主键类型差异,最简洁的方案是仓储接口层统一用string类型传递主键:
- MongoDB仓储内部通过
primitive.ObjectIDFromHex()把传入的字符串ID转为ObjectID类型再查询 - MySQL/PostgreSQL仓储内部直接用字符串UUID,或转为数字主键、数据库原生UUID类型再查询
这种方案不需要引入泛型,也不需要业务层引入任何数据库驱动的依赖,彻底隔离主键类型差异。
修正后的基础仓储接口定义如下:
package repositories import ( "context" "your_project/models" ) // 接口定义在业务侧的repositories包,所有业务层只依赖这个接口 type UserRepository interface { Read(ctx context.Context, id string) (models.User, error) }
对应的MongoDB仓储实现修正为:
package repositories/mongo import ( "context" "fmt" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "your_project/models" "your_project/repositories" ) // 编译期检查确保MongoDBRepository实现了UserRepository接口 var _ repositories.UserRepository = (*MongoDBRepository)(nil) type MongoDBRepository struct { // 客户端实例存在结构体内部,绝不对外透传 client *mongo.Client } func (mo MongoDBRepository) Read(ctx context.Context, id string) (models.User, error) { oid, err := primitive.ObjectIDFromHex(id) if err != nil { return models.User{}, fmt.Errorf("invalid mongodb object id: %w", err) } var user models.User err = mo.client.Database("your_db_name").Collection("users").FindOne(ctx, bson.M{"_id": oid}).Decode(&user) if err != nil { return models.User{}, err } return user, nil }
代码中加的var _ repositories.UserRepository = (*MongoDBRepository)(nil)是编译期校验,能提前发现方法签名不匹配的问题,避免运行时才发现没实现接口。
2. main函数初始化逻辑:用工厂模式屏蔽实现差异
不要在main函数里写死具体数据库的初始化逻辑,写一个统一的仓储工厂方法,根据配置返回对应数据库的仓储实例,返回值统一用接口类型。
首先保留原有的连接方法,补全错误处理,再实现工厂函数:
package repositories import ( "context" "errors" "time" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "your_project/configs" mongoimpl "your_project/repositories/mongo" // 后续加MySQL实现就import对应的包 ) func ConnectMongoDB() (*mongo.Client, func(), error) { client, err := mongo.NewClient(options.Client().ApplyURI(configs.MongoURI())) if err != nil { return nil, nil, err } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) err = client.Connect(ctx) if err != nil { cancel() return nil, nil, err } err = client.Ping(ctx, nil) if err != nil { cancel() _ = client.Disconnect(ctx) return nil, nil, err } cleanup := func() { cancel() _ = client.Disconnect(context.Background()) } return client, cleanup, nil } // NewUserRepository 仓储工厂,根据配置返回对应实现 func NewUserRepository() (UserRepository, func(), error) { switch configs.CurrentDBType() { case "mongodb": client, cleanup, err := ConnectMongoDB() if err != nil { return nil, nil, err } repo := mongoimpl.MongoDBRepository{client: client} return repo, cleanup, nil // 后续新增MySQL/PostgreSQL实现只需要在这里加case // case "mysql": // db, cleanup, err := ConnectMySQL() // if err != nil { // return nil, nil, err // } // return mysqlimpl.MySQLRepository{db: db}, cleanup, nil default: return nil, nil, errors.New("unsupported database type") } }
main函数里的初始化逻辑会非常干净,完全不接触具体数据库类型:
func main() { // 初始化仓储,拿到的永远是UserRepository接口类型,不需要关心底层实现 userRepo, cleanup, err := repositories.NewUserRepository() if err != nil { log.Fatalf("init repository failed: %v", err) } defer cleanup() // 程序退出自动关闭连接 // 把仓储实例传给控制器,全程传接口 userController := controllers.NewUserController(userRepo) // 后续启动路由、注册接口的逻辑和之前一致 }
3. 控制器层调用逻辑:只依赖接口,彻底解耦
控制器结构体里只存仓储接口类型的字段,绝对不要持有*mongo.Client、*sql.DB这类具体数据库连接类型,所有数据操作都通过接口方法完成。
package controllers import ( "net/http" "your_project/repositories" ) type UserController struct { userRepo repositories.UserRepository // 仅依赖接口,和具体数据库完全无关 } // 构造函数只接收接口类型,传任何数据库的仓储实现都可以正常工作 func NewUserController(repo repositories.UserRepository) *UserController { return &UserController{ userRepo: repo, } } // 示例:查询用户接口 func (uc *UserController) GetUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // 直接从请求里拿到字符串ID,不需要做任何数据库相关的类型转换 id := r.URL.Query().Get("id") user, err := uc.userRepo.Read(ctx, id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // 序列化user返回响应即可 }
核心解耦原则
- 依赖方向永远是业务层依赖抽象接口,数据库实现层依赖业务接口,不要反过来
- 所有数据库相关的逻辑(类型转换、查询语句、连接处理)全部封装在对应仓储实现内部,对外只暴露接口约定的方法
- 切换数据库时只需要两个操作:实现对应仓储接口的新版本、在工厂函数中新增对应分支,业务层代码不需要做任何修改
- 不要在业务层代码中import任何数据库驱动的包,只要出现这类import就说明出现了耦合,需要把逻辑下沉到仓储实现层
内容的提问来源于stack exchange,提问作者Alireza
相关产品推荐
相关产品推荐

