In golang, function pointers are an important type. Their main uses include callback functions, dynamic loading of library functions, etc. Therefore, their failure will have a serious impact on the correctness of the program.
However, in a real development environment, we often encounter situations where function pointers are invalid and cause strange errors in the program. This article will take a specific case as an example to analyze the reasons for function pointer failure and discuss how to avoid this situation.
Case Analysis
In a GraphQL API service, I used a third-party library to parse GraphQL query statements and use custom functions to process some of the fields. This library provides a function pointer type, we just need to pass the custom function into it.
Specifically, the code snippet is as follows:
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 }
The AddFieldResolver method is used to add a field parsing function pointer to the Resolver type structure. At the same time, this Resolver type structure also implements a GraphQL Resolver interface.
In my implementation, I added two field resolution function pointers to this Resolver type structure. These two function pointers are determined by the two field names name and created_at respectively.
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, } }
The initialization of these two field resolution function pointers is completed in the initFieldResolvers method, which will be called in the constructor of the Resolver type structure.
We also need to implement the specific Resolver interface in the Resolver type structure. The specific method is as follows:
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 }
This dynamically calls the parsing function pointer we registered before, that is, name and created_at two functions.
However, during the testing process, I found that this implementation was very unstable. Sometimes it could work normally, but at other times it reported an error "resolver not found".
Cause Analysis
In this case, I first thought of the possibility of function pointer failure. In actual development, we often encounter similar problems: storing the function pointer in a structure variable, and then calling the value of this variable elsewhere, only to find that the pointer has expired.
In Go language, like other languages, functions are first-class citizens, and function pointers are also stored as variables. Under normal circumstances, the function pointer will not expire. In theory, what is stored in the memory is a pointer address that can be called. This pointer will not be recycled until the end of the program.
However, in our scenario, since this Resolver type structure is shared among multiple coroutines, there is concurrent access, and there are also situations where the coroutine exits and releases memory. This may cause the function pointer to become invalid.
Solution
To solve the problem of function pointer failure, the essence is to avoid the situation of pointer failure. In golang's concurrent programming, there are some technical means to ensure that certain data will not go wrong when accessed concurrently. Next, we'll introduce two common techniques for avoiding invalid function pointers.
For the Resolver type structure in the above code, we can use the sync.RWMutex type to protect the fieldResolvers field Concurrent reading and writing. This way we can ensure that no race conditions will occur when reading fieldResolvers fields.
At the same time, we can also use a slice of function pointers instead of the map type. When reading the function pointer, there is no need to access the map type, thus avoiding the occurrence of race conditions.
The specific code is as follows:
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) }
Here, I changed the type of fieldResolvers from map[string]fieldResolver to []*fieldResolver. Using pointer types avoids redundant memory allocation and data copying.
Another trick to avoid invalid function pointers is to store function pointers in channels. Specifically, when we need to call a function pointer, we can send the function pointer to the channel and wait in another coroutine for the channel to return the function pointer before making the call.
In this way, it can be guaranteed that the function pointer will not expire, and problems such as memory release when the coroutine exits can be avoided.
The specific code is as follows:
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 } } }() }
Here, I changed the type of fieldResolvers to chan *fieldResolver, and called the function pointer in this channel through a coroutine.
Conclusion
For the problem of function pointer failure encountered in golang, we need to pay attention to the concurrency and memory release issues of the program. To avoid race conditions and memory management issues, we can take advantage of golang's powerful concurrent programming features, such as RWMutex and chan.
At the same time, we also need to pay attention to the use of pointer types and avoid unnecessary memory allocation and data copying to reduce the probability of function pointer failure as much as possible.
The above is the detailed content of golang function pointer invalid. For more information, please follow other related articles on the PHP Chinese website!