首页  >  文章  >  后端开发  >  golang函数指针失效

golang函数指针失效

王林
王林原创
2023-05-10 17:11:37550浏览

在 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 的并发编程中,有一些技术手段可以保证某个数据在并发访问时不会出错。接下来,我们将介绍两种常见的技巧,用于避免函数指针失效的情况。

  1. 通过复制结构体的方式来避免数据竞争

对于上述代码中的 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 。使用指针类型可以避免多余的内存分配和数据拷贝。

  1. 将函数指针存储在 channel 中

另一种避免函数指针失效的技巧是将函数指针存储在通道中。具体来说,当我们需要调用函数指针时,我们可以向通道中发送这个函数指针,并在另外的协程中等待通道返回这个函数指针之后再进行调用。

这样一来,就可以保证函数指针不会失效,并且可以避免在协程退出时的内存释放等问题。

具体代码如下:

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中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn