在 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中文網其他相關文章!