算符避坑指南:類型轉(zhuǎn)換、整除截?cái)嗯c位運(yùn)算實(shí)戰(zhàn)詳解)
運(yùn)算符這東西我學(xué) Go 的第一周覺得沒什么好講的不就是加減乘除、等于不等于那點(diǎn)事。等真正寫項(xiàng)目了才發(fā)現(xiàn)越是基礎(chǔ)的語法越藏著坑。比如 Go 里沒有三元運(yùn)算符比如整型除法直接截?cái)嘈?shù)點(diǎn)再比如^一個(gè)符號(hào)身兼按位取反和異或兩職——這些細(xì)節(jié)在 C 語言、Java、Python 里各有各的規(guī)矩到了 Go 這邊全得重新對(duì)齊。如果你是剛配好 Go 環(huán)境、或者正在看《Go Web 編程實(shí)戰(zhàn)派》這類入門書這篇學(xué)習(xí)筆記應(yīng)該能幫你把運(yùn)算符這塊一次性理順。我一直覺得學(xué)運(yùn)算符最好的方式不是死記表格而是搞懂“Go 為什么這么設(shè)計(jì)”。很多語法取舍背后都有明確的工程理由理解了理由代碼怎么寫才地道、報(bào)錯(cuò)怎么排查才快心里就有底了。這篇筆記會(huì)按算術(shù)、賦值、比較、邏輯、位運(yùn)算、其他運(yùn)算符的順序逐類拆解最后再聊優(yōu)先級(jí)和實(shí)際踩坑記錄。代碼都是可直接復(fù)制運(yùn)行的完整示例建議邊看邊動(dòng)手敲。1. 運(yùn)算符全貌先看整體地圖1.1 為什么單獨(dú)把運(yùn)算符拎出來講寫 Go 項(xiàng)目時(shí)你會(huì)發(fā)現(xiàn)運(yùn)算符不是獨(dú)立的語法點(diǎn)而是嵌在類型系統(tǒng)、語句規(guī)范、甚至編譯器約束里的。比如 Go 對(duì)類型的強(qiáng)制要求極其嚴(yán)格int和int64雖然在數(shù)學(xué)上都表示整數(shù)但直接用相加會(huì)報(bào)編譯錯(cuò)誤必須先做類型轉(zhuǎn)換。再比如自增自減、--在多數(shù)語言里是運(yùn)算符能嵌套進(jìn)表達(dá)式但 Go 把它降級(jí)成了語句。這些設(shè)計(jì)背后是 Go 工程師為了減少歧義、強(qiáng)制代碼風(fēng)格統(tǒng)一的刻意選擇。另外Go 沒有三元運(yùn)算符?:官方給出的解釋是“代碼應(yīng)當(dāng)簡單直白而不是追求表達(dá)式上的巧妙”。很多從 Python、JavaScript 轉(zhuǎn)過來的朋友一開始不習(xí)慣寫著寫著就會(huì)覺得其實(shí)強(qiáng)制寫if else反而讓邏輯更清楚代碼評(píng)審時(shí)一眼就能看懂分支走向。1.2 運(yùn)算符分類總覽Go 的運(yùn)算符可以分成六類每一類在語法和使用場景上都有明顯差異類別運(yùn)算符典型用途算術(shù)運(yùn)算符 - * / %數(shù)值計(jì)算、字符串拼接、取余取模賦值運(yùn)算符 : - * / % ^ |變量賦值、復(fù)合操作比較運(yùn)算符 ! 條件判斷、數(shù)值比較、相等性判斷邏輯運(yùn)算符 || !布爾組合、條件分流位運(yùn)算符 | ^ ^權(quán)限控制、標(biāo)志位處理、底層算法其他運(yùn)算符 * -和下標(biāo)[]、點(diǎn)號(hào).指針取值、通道收發(fā)、索引與選擇這張表后面的每行都會(huì)展開講。我的建議是第一遍看整體第二遍照著表格敲代碼第三遍再回來看優(yōu)先級(jí)基本就能把這塊吃透。2. 算術(shù)運(yùn)算符類型、整除與取余的邊界2.1 加減乘除類型相同是硬規(guī)則算術(shù)運(yùn)算符里最簡單的就是 - * /但在 Go 中必須注意參與運(yùn)算的操作數(shù)類型必須一致。int和int64雖然同樣是整型但 Go 編譯器的類型系統(tǒng)不允許混算否則報(bào)錯(cuò)invalid operation: a b (mismatched types int and int64)。我自己剛學(xué) Go 時(shí)犯過這個(gè)錯(cuò)從配置文件里讀取超時(shí)時(shí)間返回的是int64本地計(jì)算時(shí)用了int變量結(jié)果一編譯就紅。解決辦法只有一個(gè)——顯式轉(zhuǎn)換var timeout int64 30 var base int 20 result : timeout int64(base) // 把 int 轉(zhuǎn)成 int64這里有個(gè)容易忽略的細(xì)節(jié)常量參與運(yùn)算時(shí)不需要轉(zhuǎn)換因?yàn)槌A吭诰幾g期會(huì)根據(jù)上下文自動(dòng)調(diào)整類型。比如const n 10既可以直接和int算也可以和float64算。變量不行常量行這是 Go 類型系統(tǒng)里一個(gè)很有意思的設(shè)計(jì)。字符串用拼接時(shí)也遵循這個(gè)規(guī)律hello world會(huì)輸出hello world。但字符串和數(shù)字不能直接用需要借助strconv.Itoa之類的轉(zhuǎn)換函數(shù)這點(diǎn)和 JavaScript 的隱式轉(zhuǎn)換完全不同。2.2 除法與取余小心整型截?cái)嗾统ㄖ苯觼G小數(shù)點(diǎn)是新手最容易踩的坑。我在寫一個(gè)小工具時(shí)想計(jì)算“每頁顯示條數(shù)”寫了total / pageSize結(jié)果total和pageSize都是整型算出來的結(jié)果永遠(yuǎn)是整數(shù)。比如5 / 2在 Go 里直接得到2不會(huì)像 Python 那樣得到2.5。如果你確實(shí)想要精確結(jié)果把其中一個(gè)操作數(shù)轉(zhuǎn)成浮點(diǎn)型就行a, b : 5, 2 fmt.Println(float64(a) / float64(b)) // 2.5取余運(yùn)算符%要求兩邊都是整型17 % 5的結(jié)果是2。浮點(diǎn)型沒有%想算浮點(diǎn)余數(shù)用標(biāo)準(zhǔn)庫里的math.Mod或math.Remainder。兩者的細(xì)微區(qū)別在于對(duì)負(fù)數(shù)的處理方式math.Mod的結(jié)果和被除數(shù)符號(hào)一致math.Remainder則遵循 IEEE 754 規(guī)則精度上更講究。日常用math.Mod就夠了。關(guān)于負(fù)數(shù)取余Go 的規(guī)則是結(jié)果符號(hào)跟隨被除數(shù)fmt.Println(-5 % 3) // -2 fmt.Println(5 % -3) // 2這個(gè)規(guī)則本身很簡單但跨語言對(duì)比時(shí)容易暈。比如 Python 的%結(jié)果符號(hào)取的是除數(shù)方向所以-5 % 3在 Python 里是1。如果你從前端或 Python 轉(zhuǎn) Go寫取余邏輯前最好查一下當(dāng)前語言的行為別憑舊經(jīng)驗(yàn)直接套。2.3 除零問題編譯期和運(yùn)行時(shí)的差異整型除零分兩種情況如果除數(shù)是編譯期常量編譯器直接報(bào)錯(cuò)比如x : 1 / 0在編譯階段就過不去如果除數(shù)是運(yùn)行期變量比如用戶傳了個(gè) 0 進(jìn)來程序會(huì)拋出runtime panic: integer divide by zero。這個(gè) panic 會(huì)讓程序直接崩潰所以在做除法前最好加一道判斷。浮點(diǎn)型除零則不一樣1.0 / 0.0不會(huì) panic結(jié)果是正無窮Inf。這在科學(xué)計(jì)算里有時(shí)候反而有用但對(duì)業(yè)務(wù)代碼來說還是要留意是否產(chǎn)生了非預(yù)期結(jié)果。3. 賦值運(yùn)算符與自增自減細(xì)節(jié)里的語法規(guī)范3.1 復(fù)合賦值運(yùn)算符Go 支持 - * / %以及移位、位運(yùn)算對(duì)應(yīng)的復(fù)合賦值。比如a 1等價(jià)于a a 1a 2等價(jià)于a a 2。這類運(yùn)算符沒什么難以理解的地方但它們配合前面說的類型規(guī)則時(shí)要注意復(fù)合賦值不會(huì)幫你做類型轉(zhuǎn)換int64 int該報(bào)錯(cuò)還是報(bào)錯(cuò)。賦值運(yùn)算符里最有 Go 特色的是短變量聲明:。它有兩個(gè)限制一是至少左邊有一個(gè)新變量二是只能在函數(shù)內(nèi)使用。比如下面這段代碼就能通過編譯x : 1 x, y : 2, 3 // x 已存在y 是新變量所以合法如果把:用成了編譯器會(huì)提示undefined: y如果全用:而右邊又沒有新東西編譯器會(huì)提示no new variables on left side of :。3.2 自增自減是語句不是表達(dá)式Go 里i和i--是語句不是表達(dá)式。這意味著你不能寫j : i也不能寫arr[i] 1。以前在 C 里常用的while (i len) arr[i] x這種寫法在 Go 里直接報(bào)語法錯(cuò)誤。必須拆成兩行arr[i] x i單獨(dú)拆一行反而讓代碼更清晰這是 Go 刻意為之的風(fēng)格約束。另一個(gè)細(xì)節(jié)是 Go 只保留了后置i沒有前置i寫i會(huì)直接編譯失敗。這個(gè)限制讓自增自減的語義絕對(duì)統(tǒng)一避免了一大批因人而異的代碼習(xí)慣。3.3 多變量賦值與交換技巧Go 的多變量賦值非常方便最經(jīng)典的用法是交換兩個(gè)變量a, b b, a不需要第三個(gè)臨時(shí)變量編譯器會(huì)自動(dòng)處理。這在很多排序算法里能省掉一長串賦值代碼。多個(gè)返回值配合多變量賦值也很自然n, err : fmt.Println(hello) val, ok : map1[key]我實(shí)際寫代碼時(shí)更喜歡用這種語法來減少中間變量但要注意它不會(huì)修復(fù)不同類型的問題a, b : 1, x這行看起來簡單實(shí)際是兩個(gè)變量分別賦值互不干擾。4. 比較運(yùn)算符與相等性Go 的可比較類型邊界4.1 數(shù)值、字符串與布爾比較比較運(yùn)算符 ! 在數(shù)值上沒什么懸念但在字符串上需要注意比較的是字符串的內(nèi)容不是內(nèi)存地址比較的是字典序按字節(jié)逐位比較。這也是很多人從 Java 轉(zhuǎn)過來后要適應(yīng)的點(diǎn)——Go 判斷字符串是否相等不需要.equals()直接就行。布爾值只支持和!不能拿true false做比較編譯器會(huì)直接報(bào)錯(cuò)。數(shù)值比較時(shí)同樣要避開類型混用int和int64之間不能直接比較大小需要顯式轉(zhuǎn)換。4.2 可比較類型數(shù)組、結(jié)構(gòu)體和接口Go 里有些類型天然可比較除了基礎(chǔ)的數(shù)值、字符串、布爾外還有指針、數(shù)組、結(jié)構(gòu)體、channel 和接口。前提是它們內(nèi)部的所有元素或字段也是可比較的。數(shù)組的比較規(guī)則是長度相同且元素逐位相等長度不同的數(shù)組本身類型就不同不能比較。結(jié)構(gòu)體只要所有字段都可比較就能直接這個(gè)特性在對(duì)配置、狀態(tài)做斷言時(shí)非常方便type Point struct { X, Y int } p1 : Point{1, 2} p2 : Point{1, 2} fmt.Println(p1 p2) // true接口的可比較性更隱蔽。接口比較時(shí)比較的是動(dòng)態(tài)類型和動(dòng)態(tài)值如果兩個(gè)接口都指向同一個(gè)結(jié)構(gòu)體值也相同那它們的結(jié)果就是true。但如果接口里面包裹的是 slice 這種不可比較類型運(yùn)行時(shí)會(huì)直接 panic不是返回 false這點(diǎn)要特別當(dāng)心。4.3 不可比較類型slice、map、funcslice、map 和 func 類型不能直接比較只能和nil比較。我見過不少新手寫if s1 s2想比較兩個(gè)切片內(nèi)容是否相同編譯直接報(bào)錯(cuò)slice can only be compared to nil。想比較 slice 內(nèi)容需要借助reflect.DeepEqual或者手寫循環(huán)s1 : []int{1, 2, 3} s2 : []int{1, 2, 3} fmt.Println(reflect.DeepEqual(s1, s2)) // true這里提醒一句reflect.DeepEqual適合測試和臨時(shí)判斷性能開銷較大線上高頻路徑里用手寫循環(huán)更穩(wěn)妥。map 的相等性同理只能和 nil 比。這個(gè)限制背后有工程原因slice、map 是引用類型內(nèi)容可變直接做值比較在并發(fā)場景下沒有確定語義Go 選擇在語言層面直接禁止。5. 邏輯運(yùn)算符短路求值的實(shí)際意義5.1 基本規(guī)則邏輯運(yùn)算符共三個(gè)邏輯與、||邏輯或、!邏輯非。它們的操作數(shù)必須是布爾類型不像 Python 或 JavaScript 那樣接受任意真值。寫if a這種語句在 Go 里不合法必須寫成if a ! 0。這個(gè)約束讓代碼意圖非常明確但也讓習(xí)慣了隱式轉(zhuǎn)換的新手一開始總想偷懶。5.2 短路求值避免空指針的利器和||都有短路特性左邊為 false 時(shí)右邊不執(zhí)行||左邊為 true 時(shí)右邊不執(zhí)行。這個(gè)特性在判空時(shí)特別有用寫接口返回結(jié)構(gòu)體指針時(shí)我?guī)缀趺看味歼@么干if user ! nil user.Age 18 { fmt.Println(成年用戶) }如果user是 nil第一個(gè)條件已經(jīng)是 false第二個(gè)條件根本不會(huì)執(zhí)行自然不會(huì)觸發(fā) nil 解引用 panic。這也是一種優(yōu)雅的安全寫法比嵌套if user ! nil更簡潔。反過來用||做默認(rèn)值也很常見if name || strings.HasPrefix(name, tmp) { fmt.Println(需要處理) }因?yàn)槎搪芬坏﹏ame 成立后面的strings.HasPrefix不會(huì)執(zhí)行少一次無意義的函數(shù)調(diào)用。5.3 邏輯表達(dá)式簡化代碼的幾個(gè)模式熟悉短路之后代碼可以寫得很簡潔。常見模式之一是“守衛(wèi)條件 業(yè)務(wù)條件”的組合形式if opt ! nil opt.Timeout 0 check(opt.Timeout) { ... }另一個(gè)模式是用布爾條件替代 if 里的復(fù)雜嵌套。我見過不少同事把一層層 if 包成洋蔥不如把多個(gè)條件用和||平鋪開再配合括號(hào)控制優(yōu)先級(jí)代碼可讀性會(huì)提升一個(gè)檔次。不過要注意別把表達(dá)式寫太長一行超過閱讀極限反而難維護(hù)超過三四個(gè)條件就該拆變量或者提函數(shù)了。6. 位運(yùn)算符與特殊運(yùn)算符底層思維才是關(guān)鍵6.1 位運(yùn)算符的語法與用途位運(yùn)算符包括按位與、|按位或、^按位異或、左移、右移還有 Go 特有的位清除^。^這個(gè)符號(hào)在 Go 里身兼二職二元時(shí)是異或一元時(shí)是按位取反。先看移位左移 n 位相當(dāng)于乘以 2 的 n 次方右移 n 位相當(dāng)于除以 2 的 n 次方。在無符號(hào)整型上右移是邏輯右移高位補(bǔ) 0有符號(hào)整型右移是算術(shù)右移高位補(bǔ)符號(hào)位。這個(gè)差異在底層算法里能折騰死人但業(yè)務(wù)代碼很少會(huì)遇到。^是 Go 獨(dú)有的位清除意思是把左邊數(shù)中右邊數(shù)對(duì)應(yīng)位為 1 的位置清零。比如a : uint8(0b11001100) mask : uint8(0b00001111) fmt.Printf(%08b\n, a^mask) // 11000000位運(yùn)算最有價(jià)值的應(yīng)用就是權(quán)限和標(biāo)志位。用一個(gè)整型的每個(gè) bit 表示一個(gè)開關(guān)配合|打開、判斷、^關(guān)閉既省內(nèi)存又便于序列化const ( Read 1 iota // 1 Write // 2 Exec // 4 ) perm : Read | Write // 3 hasWrite : permWrite ! 0 // true perm perm ^ Exec // 移除 Exec但本來也沒有這段代碼里的iota是另一個(gè)重要關(guān)鍵字后續(xù)學(xué)習(xí)筆記里會(huì)專門展開這里先知道它能讓常量序列自動(dòng)遞增就行。6.2 其他運(yùn)算符指針、通道與下標(biāo)Go 的指針運(yùn)算符是取地址和*解引用。每天寫代碼都會(huì)遇到但核心的一點(diǎn)是 Go 指針不能做算術(shù)運(yùn)算不存在p這種把指針往后挪一個(gè)位置的操作。這避免了 C 里經(jīng)典的指針越界問題也讓 GC 能更安全地追蹤內(nèi)存。通道收發(fā)用-ch - v代表把 v 發(fā)送到通道v : -ch代表從通道接收值。通道是 Go 并發(fā)模型的核心之一運(yùn)算符本身好理解真正的難點(diǎn)是配合select、for range使用時(shí)的阻塞和關(guān)閉語義。下標(biāo)運(yùn)算符[]可以用在字符串、數(shù)組、切片、map 上.則用于選擇結(jié)構(gòu)體字段或調(diào)用方法。這些運(yùn)算符沒有太多晦澀的語法但實(shí)戰(zhàn)中它們往往組合出現(xiàn)。比如配置文件里常寫的if cfg ! nil cfg.DB ! nil cfg.DB.Addr ! { addr : cfg.DB.Addr fmt.Println(addr) }這里就把指針、邏輯短路、選擇運(yùn)算符全部串起來了。7. 運(yùn)算符優(yōu)先級(jí)背一張表少踩一半坑7.1 Go 的運(yùn)算符優(yōu)先級(jí)表Go 的優(yōu)先級(jí)規(guī)則與其他主流語言有相似處但也有關(guān)鍵差異。從高到低依次是優(yōu)先級(jí)運(yùn)算符5* / % ^4 - | ^3 ! 21||特別注意和的優(yōu)先級(jí)和乘法、取余同級(jí)這在 C 系語言里都排到加法后面了。所以1 2 3在 Go 中等價(jià)于(1 2) 3結(jié)果是7而按 C 的規(guī)則算出來是不同的??缯Z言遷移時(shí)一定要重新對(duì)照優(yōu)先級(jí)表。7.2 優(yōu)先級(jí)實(shí)戰(zhàn)示例與括號(hào)建議即使知道優(yōu)先級(jí)我依然建議在有歧義的地方加括號(hào)。優(yōu)先級(jí)是給編譯器看的括號(hào)是給人看的。比如a : 5 3*2 // 11 b : (53)*2 // 16 c : 12 3 // 7 d : (12) 3 // 7加上括號(hào)更直觀判斷表達(dá)式是否相等時(shí)優(yōu)先級(jí)更容易引發(fā)事故。a b c會(huì)先執(zhí)行再執(zhí)行實(shí)際上你得到的是a (b c)而不是(a b) c。Go 的編譯器對(duì)這種寫法通常不會(huì)給出警告但結(jié)果完全不是你以為的那樣。所以位運(yùn)算和比較運(yùn)算混用時(shí)無論優(yōu)先級(jí)如何加括號(hào)都更安全。8. 常見問題與排查技巧實(shí)錄8.1 運(yùn)算符相關(guān)的典型編譯錯(cuò)誤我挑幾個(gè)最常見的編譯報(bào)錯(cuò)分享出來基本能覆蓋日常開發(fā) 80% 的情況。第一個(gè)是類型不匹配。報(bào)錯(cuò)信息通常是invalid operation: a b (mismatched types ...)。解決方法是顯式轉(zhuǎn)換把兩個(gè)操作數(shù)統(tǒng)一成同一類型。注意不建議每次都瘋狂轉(zhuǎn)成interface{}那是放棄類型檢查的保險(xiǎn)絲大概率會(huì)給后續(xù)代碼埋坑。第二個(gè)是切片比較報(bào)錯(cuò)。報(bào)錯(cuò)信息slice can only be compared to nil。應(yīng)對(duì)方法是寫循環(huán)或用reflect.DeepEqual測試代碼用后者方便生產(chǎn)環(huán)境寫循環(huán)更穩(wěn)。第三個(gè)是自增表達(dá)式報(bào)錯(cuò)。比如j : i會(huì)直接報(bào)語法錯(cuò)誤牢記 Go 里、--是語句不是表達(dá)式拆行即可。第四個(gè)是if條件不是布爾類型。報(bào)錯(cuò)信息non-boolean condition。把if a改成if a ! 0把if str改成if str ! 就舒服了。8.2 浮點(diǎn)數(shù)比較的正確姿勢(shì)因?yàn)楦↑c(diǎn)數(shù)在內(nèi)存中用二進(jìn)制近似表示直接比較相等經(jīng)常得到假結(jié)果a : 0.1 0.2 fmt.Println(a 0.3) // false不要驚訝0.1 0.2實(shí)際值是0.30000000000000004。比較浮點(diǎn)數(shù)用誤差范圍const epsilon 1e-9 fmt.Println(math.Abs(a-0.3) epsilon) // true如果對(duì)精度要求很高比如金額計(jì)算請(qǐng)直接用整數(shù)分存儲(chǔ)或者用math/big包做高精度運(yùn)算不要依賴浮點(diǎn)等值比較。8.3 長表達(dá)式拆解與可讀性習(xí)慣最后分享一個(gè)我在 code review 中經(jīng)常強(qiáng)調(diào)的經(jīng)驗(yàn)運(yùn)算符用得花哨不等于代碼寫得好。遇到三層以上的復(fù)合表達(dá)式優(yōu)先拆變量、拆函數(shù)。比如下面這個(gè)例子一行里既有括號(hào)、位運(yùn)算、又有比較if (permWrite) ! 0 (perm2)1 1 { // ... }不如先定義幾個(gè)語義明確的布爾變量hasWrite : permWrite ! 0 hasSpecial : (perm2)1 1 if hasWrite hasSpecial { // ... }代碼的維護(hù)成本主要在閱讀讀到能清晰表達(dá)意圖的代碼比節(jié)省幾行字符重要的多。這也是為什么 Go 官方一再強(qiáng)調(diào)格式化和可讀性因?yàn)檫\(yùn)算符本身雖然簡單但組合起來、嵌套起來的復(fù)雜度可以快速失控。說回正題運(yùn)算符是 Go 語言里面最不起眼、又最繞不開的語法單元。我自己的經(jīng)驗(yàn)是每一類運(yùn)算符都要親手敲一遍極端邊界負(fù)數(shù)取余、除零、類型混用、切片比較、浮點(diǎn)相等、移位優(yōu)先級(jí)。把這些邊界都踩過一遍后面寫代碼時(shí)才能形成肌肉記憶看到報(bào)錯(cuò)就能立刻定位到是哪類運(yùn)算符的問題。這篇筆記寫到這希望能幫你少踩幾個(gè)我當(dāng)年踩過的坑。