Go如何在编译期强制函数入参必须为指针(或内部为指针的接口)
问题背景
开发类似C#带out形参的函数时(最典型的就是json.Unmarshal这类反序列化函数),我们经常需要保证传入的参数是指针或其他可写引用类型,否则函数无法修改原始传入值。
常规的实现是用反射做运行时校验,代码如下:
func mustStructPtr(f interface{}) { v := reflect.ValueOf(f) if v.Kind() != reflect.Ptr { panic(fmt.Errorf("not a pointer: %T", f)) } v = v.Elem() // 解引用指针 if v.Kind() != reflect.Struct { // 非空map、slice也属于可接受的可写类型 panic(fmt.Errorf("not struct; is %T", f)) } }
但运行时校验的问题是错误只有代码执行到对应逻辑才会触发,我们希望能在编译阶段就拦截非法调用,满足以下校验规则:
- 传入结构体指针:允许
- 传入结构体值:拦截
- 传入动态值为指针的
interface{}:允许 - 传入动态值为非指针的
interface{}:拦截 - 传入map类型:允许(map本身是引用类型,底层实现为指针)
- 传入指向slice的指针:允许
对应的合法/非法调用示例:
var myStruct MyStruct myFunc(&myStruct) // 合法:传入指针 myFunc(myStruct) // 非法:传入值类型,无法写入 var x interface{} = &myStruct myFunc(x) // 合法:接口内动态值为指针 x = myStruct myFunc(x) // 非法:接口内动态值为非指针 var y map[string]interface{} myFunc(y) // 合法:map本身是引用类型 var z = []myStruct{} myFunc(&z) // 合法:传入指向切片的指针
核心结论
当前Go的类型系统不支持100%覆盖上述所有场景的纯编译期校验。
原因有两个:一是interface{}的动态类型只有运行时才能确定,编译阶段无法获知接口内部存的值到底是不是指针;二是Go泛型不支持定义匹配「所有map类型」「所有slice类型」的通用约束,无法单独给这两类引用类型开白名单。
实际工程中可以通过「泛型拦截明确错误+轻量运行时校验补全边缘场景」的方案,覆盖99%的错误场景,这也是标准库同类函数的通用实现思路。
落地实现
1. 用泛型拦截明确的值类型传参
把核心函数定义为泛型指针接收版本,所有明确传入非指针值的调用都会在编译阶段直接报错,不需要等到运行时:
// 泛型版本,编译期直接拦截传值调用 func MyFunc[T any](v *T) { // 内部业务逻辑 // 如果需要支持map、slice直接传入,可以额外加逻辑判断v的底层Kind }
这个写法下,MyFunc(myStruct)这类明确传值的调用会直接编译不通过,从根源避免低级错误。
2. 兼容interface和map/slice场景
对于需要接收interface{}类型、或者需要直接传入map/slice的场景,写一个兼容版本,补上极轻量的运行时校验即可:
// 兼容任意类型入参的版本,补全边缘场景校验 func MyFuncAny(v any) { rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Ptr: if rv.IsNil() { panic("invalid param: nil pointer") } case reflect.Map, reflect.Slice: if rv.IsNil() { panic("invalid param: nil map/slice") } default: panic(fmt.Sprintf("invalid param type %T, expected pointer/map/slice", v)) } // 复用核心业务逻辑 }
如果不需要支持直接传入map/slice,只需要把
case reflect.Map, reflect.Slice分支去掉即可,和最初的校验逻辑保持一致。
实现说明
包括标准库json.Unmarshal在内的所有同类反序列化、输出类函数,都没有做全编译期校验,本质就是受限于Go当前的类型系统能力。实际开发中泛型版本已经能覆盖绝大多数开发者手误传值的场景,剩下的接口类型传参的错误出现概率极低,配合单测覆盖完全可以避免线上问题,没必要为了追求纯编译期校验设计出过于晦涩的API。
内容的提问来源于stack exchange,提问作者Kokizzu

