使用泛型改造 config 包

在 Go 还不支持泛型的时代,我们对于多种类型的应对一般是写出适用于各种类型的方法,然后按需要获取的类型来调用。

这样做虽然简单且有效,但大量重复的代码总是让人觉得不够优雅。而现在,Go 1.18 推出的泛型可以很好的解决这一问题。

比如 G02 课程中的 config 包,原文使用了一系列 GetStringGetBoolGetInt 的方法来支持各种类型配置的获取。

在 Go 1.18 版本中,我们可以利用泛型,只用一个方法来实现上述的效果:

func Get[T any](path string, defaultValue ...interface{}) T {
    if value := internalGet(path, defaultValue...); value != nil {
        return value.(T)
    }
    // 泛型不能返回 nil,因此需要根据类型建立空变量,这样返回的会是对应类型的"空"值
    var fallback T
    return fallback
}

在使用的时候就可以直接:

config.Get[string]("app.name")
config.Get[int]("app.level")
config.Get[bool]("app.debug")

如果你对于泛型还不了解,推荐阅读下 Go 1.18 泛型全面讲解:一篇讲清泛型的全部,个人觉得写得挺详细的。

本帖已被设为精华帖!
本帖由系统于 2年前 自动加精
sunxyw
讨论数量: 4

defaultValue ...interface{} 改成 T, 更加优化

2年前 评论

viper.Get返回的是sting类型数据,如果.env设置了配置需要取int类型,按这种写法Get[int]的话就会interface {} is string, not int

2年前 评论

这样感觉不行,类型转换那感觉有问题
比如这样的转换?

func main() {
    var t interface{} = 2.33333
    // var p = fmt.Println

    //panic
    p(t.(bool))
    p(t.(float32))
    p(t.(int))
}
2年前 评论
wuvikr

两个问题:

  1. 使用 any 进行泛型约束太宽泛了。
  2. 返回默认零值,调用者无法区分这个零值是配置项本身的值,还是因为配置项不存在而返回的默认零值。
// ConfigValue 是可以从配置中获取的类型
type ConfigValue interface {
    ~string | ~int | ~int64 | ~float64 | ~bool | ~map[string]string
}

// Get 获取配置项,返回值和是否成功
func Get[T ConfigValue](path string) (T, bool) {
    // 定义一个泛型默认值,如果没有取到值则返回零值和 false
    var fallback T

    value := viper.Get(path)
    if value == nil {
        return fallback, false
    }

    // 获取目标类型
    targetType := reflect.TypeOf(fallback)
    valueType := reflect.TypeOf(value)

    // 如果类型已经匹配,直接返回
    if valueType == targetType {
        return value.(T), true
    }

    // 尝试类型转换
    switch any(fallback).(type) {
    case string:
        // 转换为字符串
        strValue := cast.ToString(value)
        return any(strValue).(T), true
    case int:
        // 转换为整数
        intValue := cast.ToInt(value)
        return any(intValue).(T), true
    case int64:
        // 转换为整数
        int64Value := cast.ToInt64(value)
        return any(int64Value).(T), true
    case bool:
        // 转换为布尔值
        boolValue := cast.ToBool(value)
        return any(boolValue).(T), true
    case float64:
        // 转换为浮点数
        float64Value := cast.ToFloat64(value)
        return any(float64Value).(T), true
    case map[string]string:
        // 转换为字符串映射
        stringMapValue := cast.ToStringMapString(value)
        return any(stringMapValue).(T), true
    default:
        return fallback, false
    }
}

// GetWithDefault 获取配置项,如果不存在则返回默认值
func GetWithDefault[T ConfigValue](path string, defaultValue T) T {
    value, ok := Get[T](path)
    if !ok {
        return defaultValue
    }
    return value
}

调用:

// 运行服务
    err := r.Run(":" + config.GetWithDefault[string]("app.port", "8889"))
    if err != nil {
        panic(err)
    }
}

给 Get 函数添加了一个 bool 类型的返回值,调用这可以通过布尔值明确知道配置项是否存在,然后把默认配置项分离出来成为一个单独的GetWithDefault 函数,代码的逻辑更清晰,调用者在使用的时候也更灵活的去选择。
对于必须存在的初始化配置,还可以增加 MustGet函数:

// MustGet 获取配置项,如果失败则panic
func MustGet[T ConfigValue](path string) T {
    value, ok := Get[T](path)
    if !ok {
        panic(fmt.Sprintf("配置项 [%s] 不存在或类型转换失败", path))
    }
    return value
}
2个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!