<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12
      Fork me on GitHub

      小心golang中的無類型常量

      對(duì)于無類型常量,可能大家是第一次聽說,但這篇我就不放進(jìn)拾遺系列里了。

      因?yàn)殡m然名字很陌生,但我們每天都在用,每天都有無數(shù)潛在的坑被埋下。包括我本人也犯過同樣的錯(cuò)誤,當(dāng)時(shí)代碼已經(jīng)合并并發(fā)布了,當(dāng)我意識(shí)到出了什么問題的時(shí)候?yàn)闀r(shí)已晚,最后不得不多了個(gè)合并請(qǐng)求留下了丟人的黑歷史。

      為什么我要提這種塵封往事呢,因?yàn)樽罱信笥延龅搅艘粯拥膯栴},于是勾起了上面的那些“美好”回憶。于是我決定記錄一下,一來備忘,二來幫大家避坑。

      由于涉及各種隱私,朋友提問的代碼沒法放出來,但我可以給一個(gè)簡(jiǎn)單的復(fù)現(xiàn)代碼,正如我所說,這個(gè)問題是很常見的:

      package main
      
      import "fmt"
      
      type S string
      
      const (
          A S = "a"
          B   = "b"
          C   = "c"
      )
      
      func output(s S) {
          fmt.Println(s)
      }
      
      func main() {
          output(A)
          output(B)
          output(C)
      }
      

      這段代碼能正常編譯并運(yùn)行,能有什么問題?這里我就要提示你一下了,BC的類型是什么?

      你會(huì)說他們都是S類型,那你就犯了第一個(gè)錯(cuò)誤,我們用發(fā)射看看:

      fmt.Println(reflect.TypeOf(any(A)))
      fmt.Println(reflect.TypeOf(any(B)))
      fmt.Println(reflect.TypeOf(any(C)))
      

      輸出是:

      main.S
      string
      string
      

      驚不驚喜意不意外,常量的類型是由等號(hào)右邊的值推導(dǎo)出來的(iota是例外,但只能處理整型相關(guān)的),除非你顯式指定了類型。

      所以在這里B和C都是string。

      那真正的問題來了,正如我在這篇所說的,從原類型新定義的類型是獨(dú)立的類型,不能隱式轉(zhuǎn)換和賦值給原類型。

      所以這樣的代碼就是錯(cuò)的:

      func output(s S) {
          fmt.Println(s)
      }
      
      func main() {
          var a = "a" 
          output(a)
      }
      

      編譯器會(huì)報(bào)錯(cuò)。然而我們最開始的復(fù)現(xiàn)代碼是沒有報(bào)錯(cuò)的:

      const (
          A S = "a"
          B   = "b"
          C   = "c"
      )
      
      func output(s S) {
          fmt.Println(s)
      }
      

      output函數(shù)只接受S類型的值,但我們的BC都是string類型的,為什么這里可以編譯通過還正常運(yùn)行了呢?

      這就要說到golang的坑點(diǎn)之一——無類型常量了

      什么是無類型常量

      這個(gè)好理解,定義常量時(shí)沒指定類型,那就是無類型常量,比如:

      const (
          A S = "a"
          B   = "b"
          C   = "c"
      )
      

      這里A顯式指定了類型,所以不是無類型常量;而B和C沒有顯式指定類型,所以就是無類型常量(untyped constant)。

      無類型常量的特性

      無類型常量有一些特性和其他有類型的常量以及變量不一樣,得單獨(dú)講講。

      默認(rèn)的隱式類型

      正如下面的代碼里我們看到的:

      const (
          A = "a"
          B = 1
          C = 1.0
      )
      
      func main() {
          fmt.Println(reflect.TypeOf(any(A))) // string
          fmt.Println(reflect.TypeOf(any(B))) // int
          fmt.Println(reflect.TypeOf(any(C))) // float64
      }
      

      雖說我們沒給這些常量指定某個(gè)類型,但他們還是有自己的類型,和初始化他們的字面量的默認(rèn)類型相應(yīng),比如整數(shù)字面量是int,字符串字面量是string等等。

      但只有一種情況下他們才會(huì)表現(xiàn)出自己的默認(rèn)類型,也就是在上下文中沒法推斷出這個(gè)常量現(xiàn)在應(yīng)該是什么類型的時(shí)候,比如賦值給空接口。

      類型自動(dòng)匹配

      這個(gè)名字不好,是我根據(jù)它的表現(xiàn)起的,官方的名字叫Representability,直譯過來是“代表性”。

      看下這個(gè)例子:

      const delta = 1 // untyped constant, default type is int
      var num int64
      num += delta
      

      如果我們把const換成var,代碼無法編譯,會(huì)爆出這種錯(cuò)誤:invalid operation: num + delta (mismatched types int64 and int)

      但為什么常量可以呢?這就是Representability或者說類型自動(dòng)匹配在搗鬼。

      按照官方的解釋:如果一個(gè)無類型常量的值是一個(gè)類型T的有效值,那么這個(gè)常量的類型就可以是類型T

      舉個(gè)例子,int8類型的所有合法的值是[-128, 127),那么只要值在這個(gè)范圍內(nèi)的整數(shù)常量,都可以被轉(zhuǎn)換成int8。

      字符串類型同理,所有用字符串初始化的無類型常量都可以轉(zhuǎn)換成字符串以及那些基于字符串創(chuàng)建的新類型

      這就解釋了開頭那段代碼為什么沒問題:

      type S string
      
      const (
          A S = "a"
          B   = "b"
          C   = "c"
      )
      
      func output(s S) {
          fmt.Println(s)
      }
      
      func main() {
          output(A) // A 本來就是 S,自然沒問題
          output(B) // B 是無類型常量,默認(rèn)類型string,可以表示成 S,沒問題
          output(C) // C 是無類型常量,默認(rèn)類型string,可以表示成 S,沒問題
          // 下面的是有問題的,因?yàn)轭愋妥詣?dòng)匹配不會(huì)發(fā)生在無類型常量和字面量以外的地方
          // s := "string"
          // output(s)
      }
      

      也就是說,在有明確給出類型的上下文里,無類型常量會(huì)嘗試去匹配那個(gè)目標(biāo)類型T,如果常量的值符合目標(biāo)類型的要求,常量的類型就會(huì)變成目標(biāo)類型T。例子里的delta的類型就會(huì)自動(dòng)變成int64類型。

      我沒有去找為什么golang會(huì)這么設(shè)計(jì),在c++、rust和Java里常量的類型就是從初始化表達(dá)式推導(dǎo)或顯式指定的那個(gè)類型。

      一個(gè)猜測(cè)是golang的設(shè)計(jì)初衷想讓常量的行為表現(xiàn)和字面量一樣。除了兩者都有的類型自動(dòng)匹配,另一個(gè)有力證據(jù)是golang里能作為常量的只有那些能做字面類型的類型(字符串、整數(shù)、浮點(diǎn)數(shù)、復(fù)數(shù))。

      無類型常量的類型自動(dòng)匹配會(huì)帶來很有限的好處,以及很惡心的坑。

      無類型常量帶來的便利

      便利只有一個(gè),可以少些幾次類型轉(zhuǎn)換,考慮下面的例子:

      const factor = 2
      
      var result int64 = int64(num) * factor / ( (a + b + c) / factor )
      

      這樣復(fù)雜的計(jì)算表達(dá)式在數(shù)據(jù)分析和圖像處理的代碼里是很常見的,如果我們沒有自動(dòng)類型匹配,那么就需要顯式轉(zhuǎn)換factor的類型,光是想想就覺得煩人,所以我也就不寫顯式類型轉(zhuǎn)換的例子了。

      有了無類型常量,這種表達(dá)式的書寫就沒那么折磨了。

      無類型常量的坑

      說完聊勝于無的好處,下面來看看坑。

      一種常見的在golang中模擬enum的方法如下:

      type ConfigType string
      
      const (
          CONFIG_XML ConfigType = "XML"
          CONFIG_JSON = "JSON"
      )
      

      發(fā)現(xiàn)上面的問題了嗎,沒錯(cuò),只有CONFIG_XMLConfigType類型的!

      但因?yàn)闊o類型常量有自動(dòng)類型匹配,所以你的代碼目前為止運(yùn)行起來一點(diǎn)問題也沒有,這也導(dǎo)致你沒發(fā)現(xiàn)這個(gè)缺陷,直到:

      // 給enum加個(gè)方法,現(xiàn)在要能獲取常量的名字,以及他們?cè)谂渲脭?shù)組里的index
      type ConfigType string
      
      func (c ConfigType) Name() string {
          switch c {
          case CONFIG_XML:
              return "XML"
          case CONFIG_JSON:
              return "JSON"
          }
          return "invalid"
      }
      
      func (c ConfigType) Index() int {
          switch c {
          case CONFIG_XML:
              return 0
          case CONFIG_JSON:
              return 1
          }
          return -1
      }
      

      目前為止一切安好,然后代碼炸了:

      fmt.Println(CONFIG_XML.Name())
      fmt.Println(CONFIG_JSON.Name()) // !!! error
      

      編譯器不樂意,它說:CONFIG_JSON.Name undefined (type untyped string has no field or method Name)

      為什么呢,因?yàn)樯舷挛睦餂]明確指定類型,fmt.Println的參數(shù)要求都是any,所以這里用了無類型常量的默認(rèn)類型。當(dāng)然在其他地方也一樣,CONFIG_JSON.Name()這個(gè)表達(dá)式是無法推斷出CONFIG_JSON要匹配成什么類型的。

      這一切只是因?yàn)槟闵賹懥艘粋€(gè)類型。

      這還只是第一個(gè)坑,實(shí)際上因?yàn)橹灰悄繕?biāo)類型可以接受的值,就可以賦值給目標(biāo)類型,那么出現(xiàn)這種代碼也不奇怪:

      const NET_ERR_MESSAGE = "site is unreachable"
      
      func doWithConfigType(t ConfigType)
      
      doWithConfigType(CONFIG_JSON)
      doWithConfigType(NET_ERR_MESSAGE) // WTF???
      

      一不小心就能把錯(cuò)得離譜的參數(shù)傳進(jìn)去,如果你沒想到這點(diǎn)而做好防御的話,生產(chǎn)事故就理你不遠(yuǎn)了。

      第一個(gè)坑還可以通過把常量定義寫全每個(gè)都加上類型來避免,第二個(gè)就只能靠防御式編程湊活了。

      看到這里,你也應(yīng)該猜到我當(dāng)年闖的是什么禍了。好在及時(shí)發(fā)現(xiàn),最后補(bǔ)全聲明 + 防御式編程在出事故前把問題解決了。

      最后也許有人會(huì)問,golang實(shí)現(xiàn)enum這么折磨?沒有別的辦法了嗎?

      當(dāng)然有,而且有不少,其中一個(gè)比較著名的是stringer: https://pkg.go.dev/golang.org/x/tools/cmd/stringer

      這個(gè)工具也只能解決一部分問題,但至少比什么都做不了要強(qiáng)太多了。

      總結(jié)

      無類型常量會(huì)自動(dòng)轉(zhuǎn)換到匹配的類型,這會(huì)帶來意想不到的麻煩。

      一點(diǎn)建議:

      1. 如果可以的話,盡量在定義常量時(shí)給出類型,尤其是你自定義的類型,int這種看情況可以不寫
      2. 嘗試用工具去生成enum,一定要自己寫過過癮的話記得處理必然存在的例外情況。

      這就是golang的大道至簡(jiǎn),簡(jiǎn)單它自己,坑都留給你。

      參考

      https://go.dev/ref/spec#Representability

      posted @ 2023-03-20 13:21  apocelipes  閱讀(1027)  評(píng)論(11)    收藏  舉報(bào)
      主站蜘蛛池模板: 无套内谢少妇一二三四| 欧美在线观看www| 亚洲一区二区三级av| 日本一区二区中文字幕久久| 骚虎视频在线观看| 国产欧美另类久久久精品不卡 | 成年女人午夜毛片免费视频 | 亚洲精品久久久久国色天香| 丰满少妇高潮惨叫久久久| 91偷自国产一区二区三区| 蜜臀av人妻国产精品建身房| 五月丁香啪啪| 佛冈县| 亚洲av色一区二区三区| 国产熟女真实乱精品51| 超碰人人超碰人人| 国产一区二区午夜福利久久| 国产成人无码aa精品一区| 国产精品午夜福利片国产| 国产永久免费高清在线| 亚洲 欧洲 无码 在线观看| 久久毛片少妇高潮| 日本亚洲一区二区精品久久| 国产亚洲无线码一区二区| 环江| 午夜av高清在线观看| 亚洲综合天堂一区二区三区| 丰满少妇高潮无套内谢| 成熟女人特级毛片www免费| 亚洲午夜久久久影院伊人| 高颜值午夜福利在线观看| a级黑人大硬长爽猛出猛进| 少妇被黑人到高潮喷出白浆| 果冻传媒一区二区天美传媒| a毛片免费在线观看| 精品视频福利| 亚洲乱人伦中文字幕无码| 九九热视频在线精品18| 天天影视色香欲综合久久| 精品久久久久久无码免费| 精品综合一区二区三区四区|