Hero Image of content
如何给一个类型添加额外方法以满足特定接口

如果有一个实现了 A 接口,但没有实现 B 接口的类型 T,并且需要将其传递给一个期望 B 接口的函数时,应如何操作?

下面通过一个实际案例来解答这个问题:

实际场景:Gorm 日志配置

Gorm 允许配置 Logger 来记录慢查询和错误信息,提供了默认 logger 实现及自定义选项。

假设你的项目中已有一个自定义的日志接口logger.Logger,并未直接实现 Gorm 的 Logger 接口,而你又希望在 Gorm 中接入现有的日志系统,为了避免重复实现日志逻辑,你需要在 Gorm 默认的 logger 上接入你的日志系统。

问题在于,Gorm 的 logger 初始化需要一个符合其Writer接口的参数,而你的业务日志接口未实现该接口。如何解决这一问题?

解决思路借鉴:io.NopCloser

Go 标准库中的io.NopCloser提供了一个很好的灵感。通过它,任何实现了io.Reader的类型可以轻松转换为实现了io.ReadCloser的类型,即通过包装原类型,添加必要的方法(如 Close)来满足新接口。

type nopCloser struct {
    io.Reader
}

func (nopCloser) Close() error { return nil }

func NopCloser(r io.Reader) io.ReadCloser {
    return nopCloser{r}
}

应用到 Gorm 场景

我们同样可以采用包装策略,为业务日志类型添加所需功能以匹配 Gorm 的默认 logger。

// 假设这是业务日志接口
type Logger interface {
    Debug(msg string, fields ...Field)
    Info(msg string, fields ...Field)
    Warn(msg string, fields ...Field)
    Error(msg string, fields ...Field)
}

type Field struct {
    Key   string
    Value any
}

创建适配器类型

定义一个loggerWriter结构体,包含原业务 Logger,实现所需的Printf方法,从而满足glogger.Writer接口。

// glogger "gorm.io/gorm/logger"

type loggerWriter struct {
    Logger
}

// 实现Printf方法,转发给业务Logger的Debug方法
func (lw loggerWriter) Printf(msg string, args ...interface{}) {
    lw.Debug(msg, Field{Key: "args", Value: args})
}

func LoggerWriter(l Logger) glogger.Writer {
    return loggerWriter{l}
}

初始化 Gorm

通过LoggerWriter函数,将业务 Logger 适配为 Gorm 默认 logger 初始化时期望的类型。

func InitGorm(l Logger) (*gorm.DB, error) {
    newLogger := glogger.New(
        LoggerWriter(l), // 使用适配器
        glogger.Config{
            SlowThreshold:             time.Second,
            IgnoreRecordNotFoundError: true,
            ParameterizedQueries:      true,
            LogLevel:                  glogger.Info,
        },
    )
    return gorm.Open(sqlite.Open("test.db"), &gorm.Config{
        Logger: newLogger,
    })
}

动态选择日志级别

如果希望在Printf中动态选择调用的日志方法(例如使用Warn而非固定的Debug),可以采用函数类型适配。

type loggerWriterFunc func(string, ...Field)

func (l loggerWriterFunc) Printf(msg string, args ...interface{}) {
    l(msg, Field{Key: "args", Value: args}) // 这里l可以是业务日志的任意方法
}

// 初始化时,直接传入业务日志的相应方法
newLogger := glogger.New(
    loggerWriterFunc(l.Warn), // 使用Warn方法
    glogger.Config{},
)

总结

通过上述方法,我们巧妙地解决了类型接口不匹配的问题,不仅复用了现有日志系统,还保持了代码的灵活性和扩展性。