網站首頁 編程語言 正文
介紹
在 Go reflect 包里面對 Type 有一個 Comparable 的定義:
package reflect type Type interface { // Comparable reports whether values of this type are comparable. Comparable() bool }
正如字面意思,Comparable 表示一個類型是否可以直接使用運算符比較。Go spec?羅列了所有可比較的類型,其中將可比較性劃分為兩個維度(如果不符合要求,會直接在編譯期報錯):
- Comparable:可以使用?==?和?!=?比較,非黑即白
- Ordered:可以使用?>?>=?<?<=?做大小比較,有明確的大小概念
我簡單整理了一下所有 Go 內置類型的約定:
Type | Comparable | Ordered | Description |
Boolean | ? | ? | |
Integer | ? | ? | |
Float | ? | ? | |
Complex | ? | ? | 分別比較實數和虛數,同時相等則兩個復數相等。 如果需要比較大小,需要開發者分別比較實數和虛數。 |
String | ? | ? | 基于字節逐個比較。 |
Pointer | ? | ? | 如果兩個指針指向同一個對象或者都為 nil,則兩者相等。 |
Channel | ? | ? | 類似 Pointer,兩個 Channel 變量只有都為 nil,或者指向同一個 Channel 的時候才相等。 |
Interface | ? | ? | 兩個 interface 的 Type 和 Value 值同時相等時,兩者才相等。 |
Struct | ?? | ? | 僅當 Struct 內所有成員都是 Comparable,這個 Struct 才是 Comparable 的。 如果兩個 struct 類型相同,且所有非空成員變量都相等,則兩者相等。 |
Array | ?? | ? | 僅當成員為 Comparable,Array 才是 Comparable 的。 如果兩個 Array 中的每一個元素一一相等時,則兩個 Array 相等。 |
Map | ? | ? | |
Slice | ? | ? | |
Func | ? | ? |
從上面可以看到,Go 當中絕大多數類型都是可以使用運算符相互比較的,唯獨不包含 Slice,Map 和 Func,也有容器類型 Struct、Array 本身的 Comparable 取決于成員的類型。
內部實現
知道了語法約定,我們可以看一下 reflect 具體是怎么判斷一個變量的 Comparable 屬性:
type rtype struct { // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool } func (t *rtype) Comparable() bool { return t.equal != nil }
很簡單,其實就是為每一個類型配備了一個?equal?比較函數,如果有這個函數則是 comparable。
上面的 rtype 結構就包含在所有類型的內存頭部:
// emptyInterface is the header for an interface{} value. type emptyInterface struct { typ *rtype word unsafe.Pointer }
所以如果希望知道某一個類型的 equal 需要翻閱對應類型源碼。通過編譯 SSA 可以找到對應類型的比較函數。
比如在?go/src/runtime/alg.go?下可以看到 interface 的 equal 函數的具體實現:
func efaceeq(t *_type, x, y unsafe.Pointer) bool { if t == nil { return true } eq := t.equal if eq == nil { panic(errorString("comparing uncomparable type " + t.string())) } if isDirectIface(t) { // t.kind == kindDirectIface // Direct interface types are ptr, chan, map, func, and single-element structs/arrays thereof. // Maps and funcs are not comparable, so they can't reach here. // Ptrs, chans, and single-element items can be compared directly using ==. return x == y } return eq(x, y) }
現實中的陷阱與應用
在知道上面的設定之后,可以理解很多我們在開發當中碰到的錯誤。
errors.Is
我們常常在模塊內定義錯誤時,會定義出如下類型:
type CustomError struct { Metadata map[string]string Message string } func (c CustomError) Error() string { return c.Message } var ( ErrorA = CustomError{Message:"A", Matadata: map[string]string{"Reason":""}} ErrorB = CustomError{Message:"B"} ) func DoSomething() error { return ErrorA }
而我們在外部接收到錯誤之后常常會使用?errors.Is?來判斷錯誤類型:
err:=DoSomething() if errors.Is(err, ErrorA) { // handle err }
但是會發現上面這個判斷無論如何都是 false。研究一下?errors.Is?的源碼:
func Is(err, target error) bool { if target == nil { return err == target } isComparable := reflect.TypeOf(target).Comparable() for { if isComparable && err == target { return true } if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { return true } if err = errors.Unwrap(err); err == nil { return false } } }
可以看到這是一個在 error tree 上遞歸的流程,真值的終結條件是?err==target?,但是前提是?target?本身得是 comparable 的。
A comparison of two interface values with identical dynamic types causes a run-time panic if values of that type are not comparable.
如上描述,如果不加上這一段約束,會引發 panic。
所以如果我們把一個 map 放入了 error struct,就導致這個 error 變為 incomparable,永遠無法成功比較。
解決方案也很簡單,就是將 Error 定義指針類型:
var ( ErrorA = &CustomError{Message:"A", Matadata: map[string]string{"Reason":""}} ErrorB = &CustomError{Message:"B"} )
指針類型比較只需要是否檢查是否指向同一個對象,這樣就能順利比較了。
(*Type)(nil) ≠ nil
這是 Go FAQ 的其中一條:
func returnsError() error { var p *MyError = nil if bad() { p = ErrBad } return p // Will always return a non-nil error. }
上面返回的?p?永遠不會與?nil?相等。
這是為什么呢,因為 error 是一個 interface,從上面可以知道,interface 之間比較需要保證兩者的 Type 和 Value 兩兩相等:
- 語言內的?nil?可以理解為一個 Type 和 Value 均為空的 interface
- 代碼里面返回的?p?雖然 Value 為空,但是 Type 是?*MyError
所以?p!=nil?。
正確的代碼應該是這樣的:
func returnsError() error { if bad() { return ErrBad } return nil }
這個問題不僅僅是拋出錯誤的時候會出現,任何返回 interface 的場景都需要注意。
Context Value Key
Go 的 Context 可以存取一些全局變量,其存儲方式是一個樹狀結構,每一次取值的時候就會從當前節點一路遍歷到根節點,查找是否有對應的 Key:
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) }
那么就可能會出現因為子節點的 Key 與其中一個父節點的 Key 相同,導致 Value 被錯誤地覆蓋。比如:
ctx = Context.Background() ctx = context.WithValue(ctx, "key", "123") ctx = context.WithValue(ctx, "key", "456") ctx.Value("key") // 456
因為 Context 是全鏈路透傳的,誰都沒法保證一個 Key 是否會被其中某一層覆蓋。這個問題本質上是:當Key 的類型為 Integer/Float/String/Complex 時,"偽造"一個值相同的 Key 太容易了。那么我們可以運用 Go Comparable 的特性,選擇無法被"偽造"的類型作為 Key。推薦兩種比較優雅的方式:
指針類型
var key = byte(0) ctx = context.WithValue(ctx, &key, "123") ctx.Value(&key)
這樣一來,除了包內函數,沒有其他代碼還能構造出相同的指針了。
Struct 類型
從上文可以知道,strcut 只要類型相同,內部的值相等,就能直接使用?==?判斷相等,那么我們可以直接使用 struct 作為 Key。
type key struct {} ctx = context.WithValue(ctx, key{}, "123") ctx.Value(key{})
同樣的,我們把 struct 定義為私有類似,包外也無法構造出相同的 key。
我們知道空 struct 是不占用內存的,這么做相比指針類型的 Key,可以減少內存開銷。
原文鏈接:https://sorcererxw.com/articles/go-comparable-type
相關推薦
- 2023-03-01 shell耗時計算的實現_linux shell
- 2022-10-07 C++結構體中變長數組的使用問題分解刨析_C 語言
- 2023-01-30 delphi?判斷字符串是否為純字母組合的函數_Delphi
- 2023-10-26 無法加載文件 C:\Users\sundear\AppData\Roaming\npm\dva.ps
- 2022-05-11 input 輸入框居中顯示數據解決辦法
- 2022-06-09 python?Tkinter模塊使用方法詳解_python
- 2022-12-15 Redis分布式鎖如何設置超時時間_Redis
- 2022-11-04 ASP.NET?MVC解決上傳圖片臟數據的方法_實用技巧
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支