Go 1.18如何实现单方法返回不同具体类型复用配置加载逻辑
问题描述
假设我有如下代码:
type Type1 struct { Name string `json:"name,omitempty"` Path string `json:"path"` File string `json:"file"` Tag int `json:"tag"` Num int `json:"num"` } func LoadConfiguration(data []byte) (*Type1, error) { config, err := loadConf1(data) if err != nil { return nil, err } confOther, err := loadConfOther1() if err != nil { return nil, err } // do something with confOther fmt.Println("confOther", confOther) if confOther.Tag == 0 { config.Num = 5 } // do something with config attributes of type1 if config.Tag == 0 { config.Tag = 5 } if config.Num == 0 { config.Num = 4 } return config, nil } func loadConf1(bytes []byte) (*Type1, error) { config := &Type1{} if err := json.Unmarshal(bytes, config); err != nil { return nil, fmt.Errorf("cannot load config: %v", err) } return config, nil } func loadConfOther1() (*Type1, error) { // return value of this specific type flatconfig := &Type1{} // read a file as []byte // written as a fixed array to simplify this example fileContent := []byte{10, 22, 33, 44, 55} if err := json.Unmarshal(fileContent, flatconfig); err != nil { return nil, fmt.Errorf("cannot read config %v", err) } return flatconfig, nil }
上述代码中唯一的公开函数是LoadConfiguration。
这段代码改编自真实业务代码,作用是将JSON数据反序列化为特定结构体实例,部分看似冗余的逻辑是为简化示例对原代码做裁剪后保留的内容。
目前上述代码可正常运行,但我现在需要新增一个名为Type2的结构体类型,希望复用现有方法完成Type2类型的数据读取,无需全量复制粘贴原有逻辑。Type2的结构定义如下:
type Type2 struct { Name string `json:"name,omitempty"` Path string `json:"path"` Map *map[string]interface{} `json:"map"` Other string `json:"other"` }
核心需求:调用LoadConfiguration时既可以得到Type1类型的返回结果,也可以得到Type2类型的返回结果。也可以接受新增类似LoadConfiguration2的专属调用方法,但不希望重复复制loadConf1和loadConfOther1的实现逻辑,Go 1.18中有没有符合语言惯用法的实现方案?
实现方案
Go 1.18 提供的泛型特性可以直接解决这个问题,核心思路是把通用的反序列化、文件读取逻辑抽为泛型函数,类型专属的字段校验/赋值逻辑通过回调注入,完全不需要重复代码。
第一步:改造基础加载函数为泛型实现
原来的loadConf1、loadConfOther1逻辑完全和具体类型无关,只是反序列化到对应结构体,直接改造成泛型函数即可,原有逻辑完全保留:
func loadConf[T any](bytes []byte) (*T, error) { config := new(T) if err := json.Unmarshal(bytes, config); err != nil { return nil, fmt.Errorf("cannot load config: %v", err) } return config, nil } func loadConfOther[T any]() (*T, error) { flatconfig := new(T) // 原有读文件逻辑保持不变,示例里的固定字节流直接保留 fileContent := []byte{10, 22, 33, 44, 55} if err := json.Unmarshal(fileContent, flatconfig); err != nil { return nil, fmt.Errorf("cannot read config %v", err) } return flatconfig, nil }
第二步:实现通用LoadConfiguration泛型入口
注意原代码中对Tag、Num字段的操作是Type1独有的,Type2不存在这些字段,不能直接写在通用泛型逻辑中,因此把类型专属的后置处理作为参数传入,通用流程保持统一:
func LoadConfiguration[T any]( data []byte, postProcess func(mainConf *T, otherConf *T) error, ) (*T, error) { config, err := loadConf[T](data) if err != nil { return nil, err } confOther, err := loadConfOther[T]() if err != nil { return nil, err } fmt.Println("confOther", confOther) // 执行对应类型的专属处理逻辑 if err := postProcess(config, confOther); err != nil { return nil, err } return config, nil }
第三步:为不同类型实现专属加载入口
只需要把对应类型的字段处理逻辑写到回调里即可,不需要改动任何通用加载代码:
// LoadType1Config 加载Type1类型配置 func LoadType1Config(data []byte) (*Type1, error) { return LoadConfiguration[Type1](data, func(config *Type1, confOther *Type1) error { // 原有Type1的处理逻辑原封不动迁移 if confOther.Tag == 0 { config.Num = 5 } if config.Tag == 0 { config.Tag = 5 } if config.Num == 0 { config.Num = 4 } return nil }) } // LoadType2Config 加载Type2类型配置 func LoadType2Config(data []byte) (*Type2, error) { return LoadConfiguration[Type2](data, func(config *Type2, confOther *Type2) error { // 这里写Type2专属的字段处理逻辑即可,无特殊逻辑直接返回nil return nil }) }
方案优势
- 通用的反序列化、文件读取逻辑仅维护一份,没有任何重复代码
- 类型专属逻辑和类型绑定,不会出现泛型代码访问结构体不存在字段的编译错误
- 后续新增新配置类型时,只需要新增对应的专属加载入口、写自己的后置逻辑即可,不需要改动基础加载代码
- 完全符合Go泛型的设计惯用法:通用逻辑泛型化,差异逻辑通过函数参数注入,不使用反射、不做不必要的类型断言
内容的提问来源于stack exchange,提问作者Stefano Cappa
相关产品推荐
相关产品推荐

