在 golang 中,函数指针是一种重要的类型,其主要用途包括回调函数、动态加载库函数等,因而其失效将会对程序的正确性产生严重影响。
然而,在真实的开发环境中,我们经常会遇到函数指针失效导致程序出现奇怪的错误的情况。本文将以一个具体的案例为例,分析函数指针失效的原因,并探讨如何避免这种情况的发生。
案例分析
在一个 GraphQL API 的服务中,我使用了一个第三方库去解析 GraphQL 的查询语句,并使用自定义函数处理其中的某些字段。这个库提供了一个函数指针类型,我们只需要将自定义函数传入其中即可。
具体来说,代码片段如下:
type fieldResolver func(ctx context.Context, obj interface{}, args map[string]interface{}) (interface{}, error) type Resolver struct { // ... fieldResolvers map[string]fieldResolver // ... } func (r *Resolver) AddFieldResolver(fieldName string, fr fieldResolver) { r.fieldResolvers[fieldName] = fr }
AddFieldResolver 方法用于向 Resolver 类型的结构体中添加一个字段解析的函数指针。同时,这个 Resolver 类型的结构体还实现了一个 GraphQL 的 Resolver 接口。
在我的实现中,我向这个 Resolver 类型的结构体中添加了两个字段解析函数指针。这两个函数指针分别由 name 和 created_at 两个字段名决定。
func (r *Resolver) BaseQuery() QueryResolver { return &queryResolver{r} } func (r *Resolver) name(ctx context.Context, obj interface{}, f graphql.ResolveFieldParams) (interface{}, error) { // handler for name field } func (r *Resolver) created_at(ctx context.Context, obj interface{}, f graphql.ResolveFieldParams) (interface{}, error) { // handler for created_at field } func (r *Resolver) initFieldResolvers() { r.fieldResolvers = map[string]fieldResolver{ "name": r.name, "created_at": r.created_at, } }
这两个字段解析函数指针的初始化是在 initFieldResolvers 方法中完成的,这个方法将被 Resolver 类型的结构体的构造函数中调用。
我们还需要在 Resolver 类型的结构体中实现具体的 Resolver 接口,具体方法如下:
type queryResolver struct{ *Resolver } func (r *queryResolver) Name(ctx context.Context, obj *types.User) (string, error) { resolver, ok := r.fieldResolvers["name"] if !ok { return "", fmt.Errorf("resolver not found") } result, err := resolver(ctx, obj, nil) if err != nil { return "", err } return result.(string), nil } func (r *queryResolver) CreatedAt(ctx context.Context, obj *types.User) (string, error) { resolver, ok := r.fieldResolvers["created_at"] if !ok { return "", fmt.Errorf("resolver not found") } result, err := resolver(ctx, obj, nil) if err != nil { return "", err } return result.(string), nil }
这动态调用了我们之前注册过的解析函数指针,也就是 name 和 created_at 两个函数。
然而,在测试过程中,我却发现这个实现非常不稳定,有时能够正常工作,但另外的时候却报错“resolver not found”。
原因分析
对于这种情况,我首先想到了函数指针失效的可能性。在实际的开发中,我们经常会遇到类似的问题:将函数指针存储在一个结构体变量里,然后在其他地方调用这个变量的值,却发现指针已经失效了。
在 Go 语言中,和其他语言一样,函数是一等公民,而函数指针也作为一个变量进行存储。通常情况下,函数指针并不会失效,理论上在内存中保存的是一个可以被调用的指针地址,一直到程序结束这个指针才会被回收。
然而在我们的场景中,由于这个 Resolver 类型的结构体是在多个协程下共享的,存在并发访问,同时也存在协程退出释放内存的情况。这就可能导致函数指针失效的情况出现。
解决方案
解决函数指针失效的问题,本质上是要避免指针失效的情况。在 golang 的并发编程中,有一些技术手段可以保证某个数据在并发访问时不会出错。接下来,我们将介绍两种常见的技巧,用于避免函数指针失效的情况。
对于上述代码中的 Resolver 类型的结构体,我们可以使用 sync.RWMutex 类型来保护 fieldResolvers 字段的并发读写。这样我们就能保证在读取 fieldResolvers 字段的情况下,不会发生竞态条件。
同时,我们也可以使用一个函数指针的切片来代替 map 类型,在读取函数指针的情况下,不再需要对 map 类型进行访问,从而避免了竞态条件的发生。
具体代码如下:
type Resolver struct { sync.RWMutex fieldResolvers []*fieldResolver } func (r *Resolver) AddFieldResolver(fr *fieldResolver) { r.Lock() defer r.Unlock() r.fieldResolvers = append(r.fieldResolvers, fr) } func (r *Resolver) name(ctx context.Context, obj interface{}, f graphql.ResolveFieldParams) (interface{}, error) { // handler for name field } func (r *Resolver) created_at(ctx context.Context, obj interface{}, f graphql.ResolveFieldParams) (interface{}, error) { // handler for created_at field } func (r *Resolver) initFieldResolvers() { r.AddFieldResolver(&r.name) r.AddFieldResolver(&r.created_at) }
在这里,我把 fieldResolvers 的类型从 map[string]fieldResolver 改为了 []*fieldResolver 。使用指针类型可以避免多余的内存分配和数据拷贝。
另一种避免函数指针失效的技巧是将函数指针存储在通道中。具体来说,当我们需要调用函数指针时,我们可以向通道中发送这个函数指针,并在另外的协程中等待通道返回这个函数指针之后再进行调用。
这样一来,就可以保证函数指针不会失效,并且可以避免在协程退出时的内存释放等问题。
具体代码如下:
type Resolver struct { fieldResolvers chan *fieldResolver // ... } func (r *Resolver) AddFieldResolver(fr *fieldResolver) { r.fieldResolvers <- fr } func (r *Resolver) initFieldResolvers() { // ... go func() { for fr := range r.fieldResolvers { if fr != nil { // call the function pointer } } }() }
在这里,我将 fieldResolvers 的类型改为了 chan *fieldResolver,并通过一个协程调用这个通道中的函数指针。
结论
对于在 golang 中遇到的函数指针失效的问题,我们需要注意程序的并发性和内存释放问题。在避免竞态条件和内存管理的问题上,我们可以利用 golang 强大的并发编程特性,如 RWMutex 和 chan 等。
同时,我们也需要注意使用指针类型和避免不必要的内存分配和数据拷贝,以尽可能地减少函数指针失效的概率。
以上是golang函数指针失效的详细内容。更多信息请关注PHP中文网其他相关文章!