<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                # 第三章 基礎數據類型 雖然從底層而言,所有的數據都是由比特組成,但計算機一般操作的是固定大小的數,如整數、浮點數、比特數組、內存地址等。進一步將這些數組織在一起,就可表達更多的對象,例如數據包、像素點、詩歌,甚至其他任何對象。Go語言提供了豐富的數據組織形式,這依賴于Go語言內置的數據類型。這些內置的數據類型,兼顧了硬件的特性和表達復雜數據結構的便捷性。 Go語言將數據類型分為四類:基礎類型、復合類型、引用類型和接口類型。本章介紹基礎類型,包括:數字、字符串和布爾型。復合數據類型——數組(§4.1)和結構體(§4.2)——是通過組合簡單類型,來表達更加復雜的數據結構。引用類型包括指針(§2.3.2)、切片(§4.2))字典(§4.3)、函數(§5)、通道(§8),雖然數據種類很多,但它們都是對程序中一個變量或狀態的間接引用。這意味著對任一引用類型數據的修改都會影響所有該引用的拷貝。我們將在第7章介紹接口類型。 ### 3.1. 整型 Go語言的數值類型包括幾種不同大小的整形數、浮點數和復數。每種數值類型都決定了對應的大小范圍和是否支持正負符號。讓我們先從整形數類型開始介紹。 Go語言同時提供了有符號和無符號類型的整數運算。這里有int8、int16、int32和int64四種截然不同大小的有符號整形數類型,分別對應8、16、32、64bit大小的有符號整形數,與此對應的是uint8、uint16、uint32和uint64四種無符號整形數類型。 這里還有兩種一般對應特定CPU平臺機器字大小的有符號和無符號整數int和uint;其中int是應用最廣泛的數值類型。這兩種類型都有同樣的大小,32或64bit,但是我們不能對此做任何的假設;因為不同的編譯器即使在相同的硬件平臺上可能產生不同的大小。 Unicode字符rune類型是和int32等價的類型,通常用于表示一個Unicode碼點。這兩個名稱可以互換使用。同樣byte也是uint8類型的等價類型,byte類型一般用于強調數值是一個原始的數據而不是一個小的整數。 最后,還有一種無符號的整數類型uintptr,沒有指定具體的bit大小但是足以容納指針。uintptr類型只有在底層編程是才需要,特別是Go語言和C語言函數庫或操作系統接口相交互的地方。我們將在第十三章的unsafe包相關部分看到類似的例子。 不管它們的具體大小,int、uint和uintptr是不同類型的兄弟類型。其中int和int32也是不同的類型,即使int的大小也是32bit,在需要將int當作int32類型的地方需要一個顯式的類型轉換操作,反之亦然。 其中有符號整數采用2的補碼形式表示,也就是最高bit位用作表示符號位,一個n-bit的有符號數的值域是從 ?2*n*???1 到 2*n*???1???1 。無符號整數的所有bit位都用于表示非負數,值域是0到 2*n*???1 。例如,int8類型整數的值域是從-128到127,而uint8類型整數的值域是從0到255。 下面是Go語言中關于算術運算、邏輯運算和比較運算的二元運算符,它們按照先級遞減的順序的排列: ~~~ * / % << >> & &^ + - | ^ == != < <= > >= && || ~~~ 二元運算符有五種優先級。在同一個優先級,使用左優先結合規則,但是使用括號可以明確優先順序,使用括號也可以用于提升優先級,例如`mask & (1 << 28)`。 對于上表中前兩行的運算符,例如+運算符還有一個與賦值相結合的對應運算符+=,可以用于簡化賦值語句。 算術運算符+、-、`*`和`/`可以適用與于整數、浮點數和復數,但是取模運算符%僅用于整數間的運算。對于不同編程語言,%取模運算的行為可能并不相同。在Go語言中,%取模運算符的符號和被取模數的符號總是一致的,因此`-5%3`和`-5%-3`結果都是-2。除法運算符`/`的行為則依賴于操作數是否為全為整數,比如`5.0/4.0`的結果是1.25,但是5/4的結果是1,因為整數除法會向著0方向截斷余數。 如果一個算術運算的結果,不管是有符號或者是無符號的,如果需要更多的bit位才能正確表示的話,就說明計算結果是溢出了。超出的高位的bit位部分將被丟棄。如果原始的數值是有符號類型,而且最左邊的bit為是1的話,那么最終結果可能是負的,例如int8的例子: ~~~ var u uint8 = 255 fmt.Println(u, u+1, u*u) // "255 0 1" var i int8 = 127 fmt.Println(i, i+1, i*i) // "127 -128 1" ~~~ 兩個相同的整數類型可以使用下面的二元比較運算符進行比較;比較表達式的結果是布爾類型。 ~~~ == equal to != not equal to < less than <= less than or equal to > greater than >= greater than or equal to ~~~ 事實上,布爾型、數字類型和字符串等基本類型都是可比較的,也就是說兩個相同類型的值可以用==和!=進行比較。此外,整數、浮點數和字符串可以根據比較結果排序。許多其它類型的值可能是不可比較的,因此也就可能是不可排序的。對于我們遇到的每種類型,我們需要保證規則的一致性。 這里是一元的加法和減法運算符: ~~~ + 一元加法 (無效果) - 負數 ~~~ 對于整數,+x是0+x的簡寫,-x則是0-x的簡寫;對于浮點數和復數,+x就是x,-x則是x 的負數。 Go語言還提供了以下的bit位操作運算符,前面4個操作運算符并不區分是有符號還是無符號數: ~~~ & 位運算 AND | 位運算 OR ^ 位運算 XOR &^ 位清空 (AND NOT) << 左移 >> 右移 ~~~ 位操作運算符`^`作為二元運算符時是按位異或(XOR),當用作一元運算符時表示按位取反;也就是說,它返回一個每個bit位都取反的數。位操作運算符`&^`用于按位置零(AND NOT):表達式`z = x &^ y`結果z的bit位為0,如果對應y中bit位為1的話,否則對應的bit位等于x相應的bit位的值。 下面的代碼演示了如何使用位操作解釋uint8類型值的8個獨立的bit位。它使用了Printf函數的%b參數打印二進制格式的數字;其中%08b中08表示打印至少8個字符寬度,不足的前綴部分用0填充。 ~~~ var x uint8 = 1<<1 | 1<<5 var y uint8 = 1<<1 | 1<<2 fmt.Printf("%08b\n", x) // "00100010", the set {1, 5} fmt.Printf("%08b\n", y) // "00000110", the set {1, 2} fmt.Printf("%08b\n", x&y) // "00000010", the intersection {1} fmt.Printf("%08b\n", x|y) // "00100110", the union {1, 2, 5} fmt.Printf("%08b\n", x^y) // "00100100", the symmetric difference {2, 5} fmt.Printf("%08b\n", x&^y) // "00100000", the difference {5} for i := uint(0); i < 8; i++ { if x&(1<<i) != 0 { // membership test fmt.Println(i) // "1", "5" } } fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6} fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4} ~~~ (6.5節給出了一個可以遠大于一個字節的整數集的實現。) 在`x<<n`和`x>>n`移位運算中,決定了移位操作bit數部分必須是無符號數;被操作的x數可以是有符號或無符號數。算術上,一個`x<<n`左移運算等價于乘以 2*n* ,一個`x>>n`右移運算等價于除以 2*n* 。 左移運算用零填充右邊空缺的bit位,無符號數的右移運算也是用0填充左邊空缺的bit位,但是有符號數的右移運算會用符號位的值填充左邊空缺的bit位。因為這個原因,最好用無符號運算,這樣你可以將整數完全當作一個bit位模式處理。 盡管Go語言提供了無符號數和運算,即使數值本身不可能出現負數我們還是傾向于使用有符號的int類型,就像數組的長度那樣,雖然使用uint無符號類型似乎是一個更合理的選擇。事實上,內置的len函數返回一個有符號的int,我們可以像下面例子那樣處理逆序循環。 ~~~ medals := []string{"gold", "silver", "bronze"} for i := len(medals) - 1; i >= 0; i-- { fmt.Println(medals[i]) // "bronze", "silver", "gold" } ~~~ 另一個選擇對于上面的例子來說將是災難性的。如果len函數返回一個無符號數,那么i也將是無符號的uint類型,然后條件`i >= 0`則永遠為真。在三次迭代之后,也就是`i == 0`時,i--語句將不會產生-1,而是變成一個uint類型的最大值(可能是 264???1 ),然后medals[i]表達式將發生運行時panic異常(§5.9),也就是試圖訪問一個slice范圍以外的元素。 出于這個原因,無符號數往往只有在位運算或其它特殊的運算場景才會使用,就像bit集合、分析二進制文件格式或者是哈希和加密操作等。它們通常并不用于僅僅是表達非負數量的場合。 一般來說,需要一個顯式的轉換將一個值從一種類型轉化位另一種類型,并且算術和邏輯運算的二元操作中必須是相同的類型。雖然這偶爾會導致需要很長的表達式,但是它消除了所有和類型相關的問題,而且也使得程序容易理解。 在很多場景,會遇到類似下面的代碼通用的錯誤: ~~~ var apples int32 = 1 var oranges int16 = 2 var compote int = apples + oranges // compile error ~~~ 當嘗試編譯這三個語句時,將產生一個錯誤信息: ~~~ invalid operation: apples + oranges (mismatched types int32 and int16) ~~~ 這種類型不匹配的問題可以有幾種不同的方法修復,最常見方法是將它們都顯式轉型為一個常見類型: ~~~ var compote = int(apples) + int(oranges) ~~~ 如2.5節所述,對于每種類型T,如果轉換允許的話,類型轉換操作T(x)將x轉換為T類型。許多整形數之間的相互轉換并不會改變數值;它們只是告訴編譯器如何解釋這個值。但是對于將一個大尺寸的整數類型轉為一個小尺寸的整數類型,或者是將一個浮點數轉為整數,可能會改變數值或丟失精度: ~~~ f := 3.141 // a float64 i := int(f) fmt.Println(f, i) // "3.141 3" f = 1.99 fmt.Println(int(f)) // "1" ~~~ 浮點數到整數的轉換將丟失任何小數部分,然后向數軸零方向截斷。你應該避免對可能會超出目標類型表示范圍的數值類型轉換,因為截斷的行為可能依賴于具體的實現: ~~~ f := 1e100 // a float64 i := int(f) // 結果依賴于具體實現 ~~~ 任何大小的整數字面值都可以用以0開始的八進制格式書寫,例如0666;或用以0x或0X開頭的十六進制格式書寫,例如0xdeadbeef。十六進制數字可以用大寫或小寫字母。如今八進制數據通常用于POSIX操作系統上的文件訪問權限標志,十六進制數字則更強調數字值的bit位模式。 當使用fmt包打印一個數值時,我們可以用%d、%o或%x參數控制輸出的進制格式,就像下面的例子: ~~~ o := 0666 fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666" x := int64(0xdeadbeef) fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x) // Output: // 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF ~~~ 請注意fmt的兩個使用技巧。通常Printf格式化字符串包含多個%參數時將會包含對應相同數量的額外操作數,但是%之后的`[1]`副詞告訴Printf函數再次使用第一個操作數。第二,%后的`#`副詞告訴Printf在用%o、%x或%X輸出時生成0、0x或0X前綴。 字符面值通過一對單引號直接包含對應字符。最簡單的例子是ASCII中類似'a'寫法的字符面值,但是我們也可以通過轉義的數值來表示任意的Unicode碼點對應的字符,馬上將會看到這樣的例子。 字符使用`%c`參數打印,或者是用`%q`參數打印帶單引號的字符: ~~~ ascii := 'a' unicode := '國' newline := '\n' fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'" fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 國 '國'" fmt.Printf("%d %[1]q\n", newline) // "10 '\n'" ~~~ ### 3.2. 浮點數 Go語言提供了兩種精度的浮點數,float32和float64。它們的算術規范由IEEE754浮點數國際標準定義,該浮點數規范被所有現代的CPU支持。 這些浮點數類型的取值范圍可以從很微小到很巨大。浮點數的范圍極限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大數值,大約是 3.4e38;對應的math.MaxFloat64常量大約是1.8e308。它們分別能表示的最小值近似為1.4e-45和4.9e-324。 一個float32類型的浮點數可以提供大約6個十進制數的精度,而float64則可以提供約15個十進制數的精度;通常應該優先使用float64類型,因為float32類型的累計計算誤差很容易擴散,并且float32能精確表示的正整數并不是很大(譯注:因為float32的有效bit位只有23個,其它的bit位用于指數和符號;當整數大于23bit能表達的范圍時,float32的表示將出現誤差): ~~~ var f float32 = 16777216 // 1 << 24 fmt.Println(f == f+1) // "true"! ~~~ 浮點數的字面值可以直接寫小數部分,像這樣: ~~~ const e = 2.71828 // (approximately) ~~~ 小數點前面或后面的數字都可能被省略(例如.707或1.)。很小或很大的數最好用科學計數法書寫,通過e或E來指定指數部分: ~~~ const Avogadro = 6.02214129e23 // 阿伏伽德羅常數 const Planck = 6.62606957e-34 // 普朗克常數 ~~~ 用Printf函數的%g參數打印浮點數,將采用更緊湊的表示形式打印,并提供足夠的精度,但是對應表格的數據,使用%e(帶指數)或%f的形式打印可能更合適。所有的這三個打印形式都可以指定打印的寬度和控制打印精度。 ~~~ for x := 0; x < 8; x++ { fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x))) } ~~~ 上面代碼打印e的冪,打印精度是小數點后三個小數精度和8個字符寬度: ~~~ x = 0 e^x = 1.000 x = 1 e^x = 2.718 x = 2 e^x = 7.389 x = 3 e^x = 20.086 x = 4 e^x = 54.598 x = 5 e^x = 148.413 x = 6 e^x = 403.429 x = 7 e^x = 1096.633 ~~~ math包中除了提供大量常用的數學函數外,還提供了IEEE754浮點數標準中定義的特殊值的創建和測試:正無窮大和負無窮大,分別用于表示太大溢出的數字和除零的結果;還有NaN非數,一般用于表示無效的除法操作結果0/0或Sqrt(-1). ~~~ var z float64 fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN" ~~~ 函數math.IsNaN用于測試一個數是否是非數NaN,math.NaN則返回非數對應的值。雖然可以用math.NaN來表示一個非法的結果,但是測試一個結果是否是非數NaN則是充滿風險的,因為NaN和任何數都是不相等的(譯注:在浮點數中,NaN、正無窮大和負無窮大都不是唯一的,每個都有非常多種的bit模式表示): ~~~ nan := math.NaN() fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false" ~~~ 如果一個函數返回的浮點數結果可能失敗,最好的做法是用單獨的標志報告失敗,像這樣: ~~~ func compute() (value float64, ok bool) { // ... if failed { return 0, false } return result, true } ~~~ 接下來的程序演示了通過浮點計算生成的圖形。它是帶有兩個參數的z = f(x, y)函數的三維形式,使用了可縮放矢量圖形(SVG)格式輸出,SVG是一個用于矢量線繪制的XML標準。圖3.1顯示了sin(r)/r函數的輸出圖形,其中r是sqrt(x*x+y*y)。 ![](https://box.kancloud.cn/2016-01-10_5691fbe2ec7b9.png) *gopl.io/ch3/surface* ~~~ // Surface computes an SVG rendering of a 3-D surface function. package main import ( "fmt" "math" ) const ( width, height = 600, 320 // canvas size in pixels cells = 100 // number of grid cells xyrange = 30.0 // axis ranges (-xyrange..+xyrange) xyscale = width / 2 / xyrange // pixels per x or y unit zscale = height * 0.4 // pixels per z unit angle = math.Pi / 6 // angle of x, y axes (=30°) ) var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°) func main() { fmt.Printf("<svg xmlns='http://www.w3.org/2000/svg' "+ "style='stroke: grey; fill: white; stroke-width: 0.7' "+ "width='%d' height='%d'>", width, height) for i := 0; i < cells; i++ { for j := 0; j < cells; j++ { ax, ay := corner(i+1, j) bx, by := corner(i, j) cx, cy := corner(i, j+1) dx, dy := corner(i+1, j+1) fmt.Printf("<polygon points='%g,%g %g,%g %g,%g %g,%g'/>\n", ax, ay, bx, by, cx, cy, dx, dy) } } fmt.Println("</svg>") } func corner(i, j int) (float64, float64) { // Find point (x,y) at corner of cell (i,j). x := xyrange * (float64(i)/cells - 0.5) y := xyrange * (float64(j)/cells - 0.5) // Compute surface height z. z := f(x, y) // Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy). sx := width/2 + (x-y)*cos30*xyscale sy := height/2 + (x+y)*sin30*xyscale - z*zscale return sx, sy } func f(x, y float64) float64 { r := math.Hypot(x, y) // distance from (0,0) return math.Sin(r) / r } ~~~ 要注意的是corner函數返回了兩個結果,分別對應每個網格頂點的坐標參數。 要解釋這個程序是如何工作的需要一些基本的幾何學知識,但是我們可以跳過幾何學原理,因為程序的重點是演示浮點數運算。程序的本質是三個不同的坐標系中映射關系,如圖3.2所示。第一個是100x100的二維網格,對應整數整數坐標(i,j),從遠處的(0, 0)位置開始。我們從遠處向前面繪制,因此遠處先繪制的多邊形有可能被前面后繪制的多邊形覆蓋。 第二個坐標系是一個三維的網格浮點坐標(x,y,z),其中x和y是i和j的線性函數,通過平移轉換位網格單元的中心,然后用xyrange系數縮放。高度z是函數f(x,y)的值。 第三個坐標系是一個二維的畫布,起點(0,0)在左上角。畫布中點的坐標用(sx, sy)表示。我們使用等角投影將三維點 ![](https://box.kancloud.cn/2016-01-10_5691fbe30f118.png) (x,y,z)投影到二維的畫布中。畫布中從遠處到右邊的點對應較大的x值和較大的y值。并且畫布中x和y值越大,則對應的z值越小。x和y的垂直和水平縮放系數來自30度角的正弦和余弦值。z的縮放系數0.4,是一個任意選擇的參數。 對于二維網格中的每一個網格單元,main函數計算單元的四個頂點在畫布中對應多邊形ABCD的頂點,其中B對應(i,j)頂點位置,A、C和D是其它相鄰的頂點,然后輸出SVG的繪制指令。 **練習 3.1:** 如果f函數返回的是無限制的float64值,那么SVG文件可能輸出無效的多邊形元素(雖然許多SVG渲染器會妥善處理這類問題)。修改程序跳過無效的多邊形。 **練習 3.2:** 試驗math包中其他函數的渲染圖形。你是否能輸出一個egg box、moguls或a saddle圖案? **練習 3.3:** 根據高度給每個多邊形上色,那樣峰值部將是紅色(#ff0000),谷部將是藍色(#0000ff)。 **練習 3.4:** 參考1.7節Lissajous例子的函數,構造一個web服務器,用于計算函數曲面然后返回SVG數據給客戶端。服務器必須設置Content-Type頭部: ~~~ w.Header().Set("Content-Type", "image/svg+xml") ~~~ (這一步在Lissajous例子中不是必須的,因為服務器使用標準的PNG圖像格式,可以根據前面的512個字節自動輸出對應的頭部。)允許客戶端通過HTTP請求參數設置高度、寬度和顏色等參數。 ### 3.3. 復數 Go語言提供了兩種精度的復數類型:complex64和complex128,分別對應float32和float64兩種浮點數精度。內置的complex函數用于構建復數,內建的real和imag函數分別返回復數的實部和虛部: ~~~ var x complex128 = complex(1, 2) // 1+2i var y complex128 = complex(3, 4) // 3+4i fmt.Println(x*y) // "(-5+10i)" fmt.Println(real(x*y)) // "-5" fmt.Println(imag(x*y)) // "10" ~~~ 如果一個浮點數面值或一個十進制整數面值后面跟著一個i,例如3.141592i或2i,它將構成一個復數的虛部,復數的實部是0: ~~~ fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1 ~~~ 在常量算術規則下,一個復數常量可以加到另一個普通數值常量(整數或浮點數、實部或虛部),我們可以用自然的方式書寫復數,就像1+2i或與之等價的寫法2i+1。上面x和y的聲明語句還可以簡化: ~~~ x := 1 + 2i y := 3 + 4i ~~~ 復數也可以用==和!=進行相等比較。只有兩個復數的實部和虛部都相等的時候它們才是相等的(譯注:浮點數的相等比較是危險的,需要特別小心處理精度問題)。 math/cmplx包提供了復數處理的許多函數,例如求復數的平方根函數和求冪函數。 ~~~ fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)" ~~~ 下面的程序使用complex128復數算法來生成一個Mandelbrot圖像。 *gopl.io/ch3/mandelbrot* ~~~ // Mandelbrot emits a PNG image of the Mandelbrot fractal. package main import ( "image" "image/color" "image/png" "math/cmplx" "os" ) func main() { const ( xmin, ymin, xmax, ymax = -2, -2, +2, +2 width, height = 1024, 1024 ) img := image.NewRGBA(image.Rect(0, 0, width, height)) for py := 0; py < height; py++ { y := float64(py)/height*(ymax-ymin) + ymin for px := 0; px < width; px++ { x := float64(px)/width*(xmax-xmin) + xmin z := complex(x, y) // Image point (px, py) represents complex value z. img.Set(px, py, mandelbrot(z)) } } png.Encode(os.Stdout, img) // NOTE: ignoring errors } func mandelbrot(z complex128) color.Color { const iterations = 200 const contrast = 15 var v complex128 for n := uint8(0); n < iterations; n++ { v = v*v + z if cmplx.Abs(v) > 2 { return color.Gray{255 - contrast*n} } } return color.Black } ~~~ 用于遍歷1024x1024圖像每個點的兩個嵌套的循環對應-2到+2區間的復數平面。程序反復測試每個點對應復數值平方值加一個增量值對應的點是否超出半徑為2的圓。如果超過了,通過根據預設置的逃逸迭代次數對應的灰度顏色來代替。如果不是,那么該點屬于Mandelbrot集合,使用黑色顏色標記。最終程序將生成的PNG格式分形圖像圖像輸出到標準輸出,如圖3.3所示。 ![](https://box.kancloud.cn/2016-01-10_5691fbe320df3.png) **練習 3.5:** 實現一個彩色的Mandelbrot圖像,使用image.NewRGBA創建圖像,使用color.RGBA或color.YCbCr生成顏色。 **練習 3.6:** 升采樣技術可以降低每個像素對計算顏色值和平均值的影響。簡單的方法是將每個像素分層四個子像素,實現它。 **練習 3.7:** 另一個生成分形圖像的方式是使用牛頓法來求解一個復數方程,例如 *z*4???1?=?0 。每個起點到四個根的迭代次數對應陰影的灰度。方程根對應的點用顏色表示。 **練習 3.8:** 通過提高精度來生成更多級別的分形。使用四種不同精度類型的數字實現相同的分形:complex64、complex128、big.Float和big.Rat。(后面兩種類型在math/big包聲明。Float是有指定限精度的浮點數;Rat是無效精度的有理數。)它們間的性能和內存使用對比如何?當渲染圖可見時縮放的級別是多少? **練習 3.9:** 編寫一個web服務器,用于給客戶端生成分形的圖像。運行客戶端用過HTTP參數參數指定x,y和zoom參數。 ### 3.4. 布爾型 一個布爾類型的值只有兩種:true和false。if和for語句的條件部分都是布爾類型的值,并且==和<等比較操作也會產生布爾型的值。一元操作符`!`對應邏輯非操作,因此`!true`的值為`false`,更羅嗦的說法是`(!true==false)==true`,雖然表達方式不一樣,不過我們一般會采用簡潔的布爾表達式,就像用x來表示`x==true`。 布爾值可以和&&(AND)和||(OR)操作符結合,并且可能會有短路行為:如果運算符左邊值已經可以確定整個布爾表達式的值,那么運算符右邊的值將不在被求值,因此下面的表達式總是安全的: ~~~ s != "" && s[0] == 'x' ~~~ 其中s[0]操作如果應用于空字符串將會導致panic異常。 因為`&&`的優先級比`||`高(助記:`&&`對應邏輯乘法,`||`對應邏輯加法,乘法比加法優先級要高),下面形式的布爾表達式是不需要加小括弧的: ~~~ if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { // ...ASCII letter or digit... } ~~~ 布爾值并不會隱式轉換為數字值0或1,反之亦然。必須使用一個顯式的if語句輔助轉換: ~~~ i := 0 if b { i = 1 } ~~~ 如果需要經常做類似的轉換, 包裝成一個函數會更方便: ~~~ // btoi returns 1 if b is true and 0 if false. func btoi(b bool) int { if b { return 1 } return 0 } ~~~ 數字到布爾型的逆轉換則非常簡單, 不過為了保持對稱, 我們也可以包裝一個函數: ~~~ // itob reports whether i is non-zero. func itob(i int) bool { return i != 0 } ~~~ ### 3.5. 字符串 一個字符串是一個不可改變的字節序列。字符串可以包含任意的數據,包括byte值0,但是通常是用來包含人類可讀的文本。文本字符串通常被解釋為采用UTF8編碼的Unicode碼點(rune)序列,我們稍后會詳細討論這個問題。 內置的len函數可以返回一個字符串中的字節數目(不是rune字符數目),索引操作s[i]返回第i個字節的字節值,i必須滿足0 ≤ i< len(s)條件約束。 ~~~ s := "hello, world" fmt.Println(len(s)) // "12" fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w') ~~~ 如果試圖訪問超出字符串索引范圍的字節將會導致panic異常: ~~~ c := s[len(s)] // panic: index out of range ~~~ 第i個字節并不一定是字符串的第i個字符,因為對于非ASCII字符的UTF8編碼會要兩個或多個字節。我們先簡單說下字符的工作方式。 子字符串操作s[i:j]基于原始的s字符串的第i個字節開始到第j個字節(并不包含j本身)生成一個新字符串。生成的新字符串將包含j-i個字節。 ~~~ fmt.Println(s[0:5]) // "hello" ~~~ 同樣,如果索引超出字符串范圍或者j小于i的話將導致panic異常。 不管i還是j都可能被忽略,當它們被忽略時將采用0作為開始位置,采用len(s)作為結束的位置。 ~~~ fmt.Println(s[:5]) // "hello" fmt.Println(s[7:]) // "world" fmt.Println(s[:]) // "hello, world" ~~~ 其中+操作符將兩個字符串鏈接構造一個新字符串: ~~~ fmt.Println("goodbye" + s[5:]) // "goodbye, world" ~~~ 字符串可以用==和<進行比較;比較通過逐個字節比較完成的,因此比較的結果是字符串自然編碼的順序。 字符串的值是不可變的:一個字符串包含的字節序列永遠不會被改變,當然我們也可以給一個字符串變量分配一個新字符串值。可以像下面這樣將一個字符串追加到另一個字符串: ~~~ s := "left foot" t := s s += ", right foot" ~~~ 這并不會導致原始的字符串值被改變,但是變量s將因為+=語句持有一個新的字符串值,但是t依然是包含原先的字符串值。 ~~~ fmt.Println(s) // "left foot, right foot" fmt.Println(t) // "left foot" ~~~ 因為字符串是不可修改的,因此嘗試修改字符串內部數據的操作也是被禁止的: ~~~ s[0] = 'L' // compile error: cannot assign to s[0] ~~~ 不變性意味如果兩個字符串共享相同的底層數據的話也是安全的,這使得復制任何長度的字符串代價是低廉的。同樣,一個字符串s和對應的子字符串切片s[7:]的操作也可以安全地共享相同的內存,因此字符串切片操作代價也是低廉的。在這兩種情況下都沒有必要分配新的內存。 圖3.4演示了一個字符串和兩個字串共享相同的底層數據。 ### 3.5.1. 字符串面值 字符串值也可以用字符串面值方式編寫,只要將一系列字節序列包含在雙引號即可: ~~~ "Hello, 世界" ~~~ ![](https://box.kancloud.cn/2016-03-12_56e3b60a4b3b6.png) 因為Go語言源文件總是用UTF8編碼,并且Go語言的文本字符串也以UTF8編碼的方式處理,因此我們可以將Unicode碼點也寫到字符串面值中。 在一個雙引號包含的字符串面值中,可以用以反斜杠`\`開頭的轉義序列插入任意的數據。下面的換行、回車和制表符等是常見的ASCII控制代碼的轉義方式: ~~~ \a 響鈴 \b 退格 \f 換頁 \n 換行 \r 回車 \t 制表符 \v 垂直制表符 \' 單引號 (只用在 '\'' 形式的rune符號面值中) \" 雙引號 (只用在 "..." 形式的字符串面值中) \\ 反斜杠 ~~~ 可以通過十六進制或八進制轉義在字符串面值包含任意的字節。一個十六進制的轉義形式是,其中兩個h表示十六進制數字(大寫或小寫都可以)。一個八進制轉義形式是,包含三個八進制的o數字(0到7),但是不能超過`\377`(譯注:對應一個字節的范圍,十進制為255)。每一個單一的字節表達一個特定的值。稍后我們將看到如何將一個Unicode碼點寫到字符串面值中。 一個原生的字符串面值形式是`...`,使用反引號`代替雙引號。在原生的字符串面值中,沒有轉義操作;全部的內容都是字面的意思,包含退格和換行,因此一個程序中的原生字符串面值可能跨越多行(譯注:在原生字符串面值內部是無法直接寫`字符的,可以用八進制或十六進制轉義或+"```"鏈接字符串常量完成)。唯一的特殊處理是會刪除回車以保證在所有平臺上的值都是一樣的,包括那些把回車也放入文本文件的系統(譯注:Windows系統會把回車和換行一起放入文本文件中)。 原生字符串面值用于編寫正則表達式會很方便,因為正則表達式往往會包含很多反斜杠。原生字符串面值同時被廣泛應用于HTML模板、JSON面值、命令行提示信息以及那些需要擴展到多行的場景。 ~~~ const GoUsage = `Go is a tool for managing Go source code. Usage: go command [arguments] ...` ~~~ ### 3.5.2. Unicode 在很久以前,世界還是比較簡單的,起碼計算機世界就只有一個ASCII字符集:美國信息交換標準代碼。ASCII,更準確地說是美國的ASCII,使用7bit來表示128個字符:包含英文字母的大小寫、數字、各種標點符號和設置控制符。對于早期的計算機程序來說,這些就足夠了,但是這也導致了世界上很多其他地區的用戶無法直接使用自己的符號系統。隨著互聯網的發展,混合多種語言的數據變得很常見(譯注:比如本身的英文原文或中文翻譯都包含了ASCII、中文、日文等多種語言字符)。如何有效處理這些包含了各種語言的豐富多樣的文本數據呢? 答案就是使用Unicode( http://unicode.org ),它收集了這個世界上所有的符號系統,包括重音符號和其它變音符號,制表符和回車符,還有很多神秘的符號,每個符號都分配一個唯一的Unicode碼點,Unicode碼點對應Go語言中的rune整數類型(譯注:rune是int32等價類型)。 在第八版本的Unicode標準收集了超過120,000個字符,涵蓋超過100多種語言。這些在計算機程序和數據中是如何體現的呢?通用的表示一個Unicode碼點的數據類型是int32,也就是Go語言中rune對應的類型;它的同義詞rune符文正是這個意思。 我們可以將一個符文序列表示為一個int32序列。這種編碼方式叫UTF-32或UCS-4,每個Unicode碼點都使用同樣的大小32bit來表示。這種方式比較簡單統一,但是它會浪費很多存儲空間,因為大數據計算機可讀的文本是ASCII字符,本來每個ASCII字符只需要8bit或1字節就能表示。而且即使是常用的字符也遠少于65,536個,也就是說用16bit編碼方式就能表達常用字符。但是,還有其它更好的編碼方法嗎? ### 3.5.3. UTF-8 UTF8是一個將Unicode碼點編碼為字節序列的變長編碼。UTF8編碼由Go語言之父Ken Thompson和Rob Pike共同發明的,現在已經是Unicode的標準。UTF8編碼使用1到4個字節來表示每個Unicode碼點,ASCII部分字符只使用1個字節,常用字符部分使用2或3個字節表示。每個符號編碼后第一個字節的高端bit位用于表示總共有多少編碼個字節。如果第一個字節的高端bit為0,則表示對應7bit的ASCII字符,ASCII字符每個字符依然是一個字節,和傳統的ASCII編碼兼容。如果第一個字節的高端bit是110,則說明需要2個字節;后續的每個高端bit都以10開頭。更大的Unicode碼點也是采用類似的策略處理。 ~~~ 0xxxxxxx runes 0-127 (ASCII) 110xxxxx 10xxxxxx 128-2047 (values <128 unused) 1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused) ~~~ 變長的編碼無法直接通過索引來訪問第n個字符,但是UTF8編碼獲得了很多額外的優點。首先UTF8編碼比較緊湊,完全兼容ASCII碼,并且可以自動同步:它可以通過向前回朔最多2個字節就能確定當前字符編碼的開始字節的位置。它也是一個前綴編碼,所以當從左向右解碼時不會有任何歧義也并不需要向前查看(譯注:像GBK之類的編碼,如果不知道起點位置則可能會出現歧義)。沒有任何字符的編碼是其它字符編碼的子串,或是其它編碼序列的字串,因此搜索一個字符時只要搜索它的字節編碼序列即可,不用擔心前后的上下文會對搜索結果產生干擾。同時UTF8編碼的順序和Unicode碼點的順序一致,因此可以直接排序UTF8編碼序列。同時因為沒有嵌入的NUL(0)字節,可以很好地兼容那些使用NUL作為字符串結尾的編程語言。 Go語言的源文件采用UTF8編碼,并且Go語言處理UTF8編碼的文本也很出色。unicode包提供了諸多處理rune字符相關功能的函數(比如區分字母和數組,或者是字母的大寫和小寫轉換等),unicode/utf8包則提供了用于rune字符序列的UTF8編碼和解碼的功能。 有很多Unicode字符很難直接從鍵盤輸入,并且還有很多字符有著相似的結構;有一些甚至是不可見的字符(譯注:中文和日文就有很多相似但不同的字)。Go語言字符串面值中的Unicode轉義字符讓我們可以通過Unicode碼點輸入特殊的字符。有兩種形式:16bit的碼點值,32bit的碼點值,其中h是一個十六進制數字;一般很少需要使用32bit的形式。每一個對應碼點的UTF8編碼。例如:下面的字母串面值都表示相同的值: ~~~ "世界" "\xe4\xb8\x96\xe7\x95\x8c" "\u4e16\u754c" "\U00004e16\U0000754c" ~~~ 上面三個轉義序列都為第一個字符串提供替代寫法,但是它們的值都是相同的。 Unicode轉義也可以使用在rune字符中。下面三個字符是等價的: ~~~ '世' '\u4e16' '\U00004e16' ~~~ 對于小于256碼點值可以寫在一個十六進制轉義字節中,例如'41'對應字符'A',但是對于更大的碼點則必須使用。因此,'4896'并不是一個合法的rune字符,雖然這三個字節對應一個有效的UTF8編碼的碼點。 得益于UTF8編碼優良的設計,諸多字符串操作都不需要解碼操作。我們可以不用解碼直接測試一個字符串是否是另一個字符串的前綴: ~~~ func HasPrefix(s, prefix string) bool { return len(s) >= len(prefix) && s[:len(prefix)] == prefix } ~~~ 或者是后綴測試: ~~~ func HasSuffix(s, suffix string) bool { return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix } ~~~ 或者是包含子串測試: ~~~ func Contains(s, substr string) bool { for i := 0; i < len(s); i++ { if HasPrefix(s[i:], substr) { return true } } return false } ~~~ 對于UTF8編碼后文本的處理和原始的字節處理邏輯是一樣的。但是對應很多其它編碼則并不是這樣的。(上面的函數都來自strings字符串處理包,真實的代碼包含了一個用哈希技術優化的Contains 實現。) 另一方面,如果我們真的關心每個Unicode字符,我們可以使用其它處理方式。考慮前面的第一個例子中的字符串,它包混合了中西兩種字符。圖3.5展示了它的內存表示形式。字符串包含13個字節,以UTF8形式編碼,但是只對應9個Unicode字符: ~~~ import "unicode/utf8" s := "Hello, 世界" fmt.Println(len(s)) // "13" fmt.Println(utf8.RuneCountInString(s)) // "9" ~~~ 為了處理這些真實的字符,我們需要一個UTF8解碼器。unicode/utf8包提供了該功能,我們可以這樣使用: ~~~ for i := 0; i < len(s); { r, size := utf8.DecodeRuneInString(s[i:]) fmt.Printf("%d\t%c\n", i, r) i += size } ~~~ 每一次調用DecodeRuneInString函數都返回一個r和長度,r對應字符本身,長度對應r采用UTF8編碼后的編碼字節數目。長度可以用于更新第i個字符在字符串中的字節索引位置。但是這種編碼方式是笨拙的,我們需要更簡潔的語法。幸運的是,Go語言的range循環在處理字符串的時候,會自動隱式解碼UTF8字符串。下面的循環運行如圖3.5所示;需要注意的是對于非ASCII,索引更新的步長將超過1個字節。 ![](https://box.kancloud.cn/2016-03-12_56e3b60a62cb3.png) ~~~ for i, r := range "Hello, 世界" { fmt.Printf("%d\t%q\t%d\n", i, r, r) } ~~~ 我們可以使用一個簡單的循環來統計字符串中字符的數目,像這樣: ~~~ n := 0 for _, _ = range s { n++ } ~~~ 像其它形式的循環那樣,我們也可以忽略不需要的變量: ~~~ n := 0 for range s { n++ } ~~~ 或者我們可以直接調用utf8.RuneCountInString(s)函數。 正如我們前面提到的,文本字符串采用UTF8編碼只是一種慣例,但是對于循環的真正字符串并不是一個慣例,這是正確的。如果用于循環的字符串只是一個普通的二進制數據,或者是含有錯誤編碼的UTF8數據,將會發送什么呢? 每一個UTF8字符解碼,不管是顯式地調用utf8.DecodeRuneInString解碼或是在range循環中隱式地解碼,如果遇到一個錯誤的UTF8編碼輸入,將生成一個特別的Unicode字符'',在印刷中這個符號通常是一個黑色六角或鉆石形狀,里面包含一個白色的問號"?"。當程序遇到這樣的一個字符,通常是一個危險信號,說明輸入并不是一個完美沒有錯誤的UTF8字符串。 UTF8字符串作為交換格式是非常方便的,但是在程序內部采用rune序列可能更方便,因為rune大小一致,支持數組索引和方便切割。 string接受到[]rune的類型轉換,可以將一個UTF8編碼的字符串解碼為Unicode字符序列: ~~~ // "program" in Japanese katakana s := "プログラム" fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0" r := []rune(s) fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]" ~~~ (在第一個Printf中的`% x`參數用于在每個十六進制數字前插入一個空格。) 如果是將一個[]rune類型的Unicode字符slice或數組轉為string,則對它們進行UTF8編碼: ~~~ fmt.Println(string(r)) // "プログラム" ~~~ 將一個整數轉型為字符串意思是生成以只包含對應Unicode碼點字符的UTF8字符串: ~~~ fmt.Println(string(65)) // "A", not "65" fmt.Println(string(0x4eac)) // "京" ~~~ 如果對應碼點的字符是無效的,則用''無效字符作為替換: ~~~ fmt.Println(string(1234567)) // "?" ~~~ ### 3.5.4. 字符串和Byte切片 標準庫中有四個包對字符串處理尤為重要:bytes、strings、strconv和unicode包。strings包提供了許多如字符串的查詢、替換、比較、截斷、拆分和合并等功能。 bytes包也提供了很多類似功能的函數,但是針對和字符串有著相同結構的[]byte類型。因為字符串是只讀的,因此逐步構建字符串會導致很多分配和復制。在這種情況下,使用bytes.Buffer類型將會更有效,稍后我們將展示。 strconv包提供了布爾型、整型數、浮點數和對應字符串的相互轉換,還提供了雙引號轉義相關的轉換。 unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等類似功能,它們用于給字符分類。每個函數有一個單一的rune類型的參數,然后返回一個布爾值。而像ToUpper和ToLower之類的轉換函數將用于rune字符的大小寫轉換。所有的這些函數都是遵循Unicode標準定義的字母、數字等分類規范。strings包也有類似的函數,它們是ToUpper和ToLower,將原始字符串的每個字符都做相應的轉換,然后返回新的字符串。 下面例子的basename函數靈感于Unix shell的同名工具。在我們實現的版本中,basename(s)將看起來像是系統路徑的前綴刪除,同時將看似文件類型的后綴名部分刪除: ~~~ fmt.Println(basename("a/b/c.go")) // "c" fmt.Println(basename("c.d.go")) // "c.d" fmt.Println(basename("abc")) // "abc" ~~~ 第一個版本并沒有使用任何庫,全部手工硬編碼實現: *gopl.io/ch3/basename1* ~~~ // basename removes directory components and a .suffix. // e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c func basename(s string) string { // Discard last '/' and everything before. for i := len(s) - 1; i >= 0; i-- { if s[i] == '/' { s = s[i+1:] break } } // Preserve everything before last '.'. for i := len(s) - 1; i >= 0; i-- { if s[i] == '.' { s = s[:i] break } } return s } ~~~ 簡化個版本使用了strings.LastIndex庫函數: *gopl.io/ch3/basename2* ~~~ func basename(s string) string { slash := strings.LastIndex(s, "/") // -1 if "/" not found s = s[slash+1:] if dot := strings.LastIndex(s, "."); dot >= 0 { s = s[:dot] } return s } ~~~ path和path/filepath包提供了關于文件路徑名更一般的函數操作。使用斜杠分隔路徑可以在任何操作系統上工作。斜杠本身不應該用于文件名,但是在其他一些領域可能會用于文件名,例如URL路徑組件。相比之下,path/filepath包則使用操作系統本身的路徑規則,例如POSIX系統使用/foo/bar,而Microsoft Windows使用c:。 讓我們繼續另一個字符串的例子。函數的功能是將一個表示整值的字符串,每隔三個字符插入一個逗號分隔符,例如“12345”處理后成為“12,345”。這個版本只適用于整數類型;支持浮點數類型的支持留作練習。 *gopl.io/ch3/comma* ~~~ // comma inserts commas in a non-negative decimal integer string. func comma(s string) string { n := len(s) if n <= 3 { return s } return comma(s[:n-3]) + "," + s[n-3:] } ~~~ 輸入comma函數的參數是一個字符串。如果輸入字符串的長度小于或等于3的話,則不需要插入逗分隔符。否則,comma函數將在最后三個字符前位置將字符串切割為兩個兩個子串并插入逗號分隔符,然后通過遞歸調用自身來出前面的子串。 一個字符串是包含的只讀字節數組,一旦創建,是不可變的。相比之下,一個字節slice的元素則可以自由地修改。 字符串和字節slice之間可以相互轉換: ~~~ s := "abc" b := []byte(s) s2 := string(b) ~~~ 從概念上講,一個[]byte(s)轉換是分配了一個新的字節數組用于保存字符串數據的拷貝,然后引用這個底層的字節數組。編譯器的優化可以避免在一些場景下分配和復制字符串數據,但總的來說需要確保在變量b被修改的情況下,原始的s字符串也不會改變。將一個字節slice轉到字符串的string(b)操作則是構造一個字符串拷貝,以確保s2字符串是只讀的。 為了避免轉換中不必要的內存分配,bytes包和strings同時提供了許多實用函數。下面是strings包中的六個函數: ~~~ func Contains(s, substr string) bool func Count(s, sep string) int func Fields(s string) []string func HasPrefix(s, prefix string) bool func Index(s, sep string) int func Join(a []string, sep string) string ~~~ bytes包中也對應的六個函數: ~~~ func Contains(b, subslice []byte) bool func Count(s, sep []byte) int func Fields(s []byte) [][]byte func HasPrefix(s, prefix []byte) bool func Index(s, sep []byte) int func Join(s [][]byte, sep []byte) []byte ~~~ 它們之間唯一的區別是字符串類型參數被替換成了字節slice類型的參數。 bytes包還提供了Buffer類型用于字節slice的緩存。一個Buffer開始是空的,但是隨著string、byte或[]byte等類型數據的寫入可以動態增長,一個bytes.Buffer變量并不需要處理化,因為零值也是有效的: *gopl.io/ch3/printints* ~~~ // intsToString is like fmt.Sprint(values) but adds commas. func intsToString(values []int) string { var buf bytes.Buffer buf.WriteByte('[') for i, v := range values { if i > 0 { buf.WriteString(", ") } fmt.Fprintf(&buf, "%d", v) } buf.WriteByte(']') return buf.String() } func main() { fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]" } ~~~ 當向bytes.Buffer添加任意字符的UTF8編碼時,最好使用bytes.Buffer的WriteRune方法,但是WriteByte方法對于寫入類似'['和']'等ASCII字符則會更加有效。 bytes.Buffer類型有著很多實用的功能,我們在第七章討論接口時將會涉及到,我們將看看如何將它用作一個I/O的輸入和輸出對象,例如當做Fprintf的io.Writer輸出對象,或者當作io.Reader類型的輸入源對象。 **練習 3.10:** 編寫一個非遞歸版本的comma函數,使用bytes.Buffer代替字符串鏈接操作。 **練習 3.11:** 完善comma函數,以支持浮點數處理和一個可選的正負號的處理。 **練習 3.12:** 編寫一個函數,判斷兩個字符串是否是是相互打亂的,也就是說它們有著相同的字符,但是對應不同的順序。 ### 3.5.5. 字符串和數字的轉換 除了字符串、字符、字節之間的轉換,字符串和數值之間的轉換也比較常見。由strconv包提供這類轉換功能。 將一個整數轉為字符串,一種方法是用fmt.Sprintf返回一個格式化的字符串;另一個方法是用strconv.Itoa(“整數到ASCII”): ~~~ x := 123 y := fmt.Sprintf("%d", x) fmt.Println(y, strconv.Itoa(x)) // "123 123" ~~~ FormatInt和FormatUint函數可以用不同的進制來格式化數字: ~~~ fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011" ~~~ fmt.Printf函數的%b、%d、%o和%x等參數提供功能往往比strconv包的Format函數方便很多,特別是在需要包含附加額外信息的時候: ~~~ s := fmt.Sprintf("x=%b", x) // "x=1111011" ~~~ 如果要將一個字符串解析為整數,可以使用strconv包的Atoi或ParseInt函數,還有用于解析無符號整數的ParseUint函數: ~~~ x, err := strconv.Atoi("123") // x is an int y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits ~~~ ParseInt函數的第三個參數是用于指定整型數的大小;例如16表示int16,0則表示int。在任何情況下,返回的結果y總是int64類型,你可以通過強制類型轉換將它轉為更小的整數類型。 有時候也會使用fmt.Scanf來解析輸入的字符串和數字,特別是當字符串和數字混合在一行的時候,它可以靈活處理不完整或不規則的輸入。 ### 3.6. 常量 常量表達式的值在編譯期計算,而不是在運行期。每種常量的潛在類型都是基礎類型:boolean、string或數字。 一個常量的聲明語句定義了常量的名字,和變量的聲明語法類似,常量的值不可修改,這樣可以防止在運行期被意外或惡意的修改。例如,常量比變量更適合用于表達像π之類的數學常數,因為它們的值不會發生變化: ~~~ const pi = 3.14159 // approximately; math.Pi is a better approximation ~~~ 和變量聲明一樣,可以批量聲明多個常量;這比較適合聲明一組相關的常量: ~~~ const ( e = 2.71828182845904523536028747135266249775724709369995957496696763 pi = 3.14159265358979323846264338327950288419716939937510582097494459 ) ~~~ 所有常量的運算都可以在編譯期完成,這樣可以減少運行時的工作,也方便其他編譯優化。當操作數是常量時,一些運行時的錯誤也可以在編譯時被發現,例如整數除零、字符串索引越界、任何導致無效浮點數的操作等。 常量間的所有算術運算、邏輯運算和比較運算的結果也是常量,對常量的類型轉換操作或以下函數調用都是返回常量結果:len、cap、real、imag、complex和unsafe.Sizeof(§13.1)。 因為它們的值是在編譯期就確定的,因此常量可以是構成類型的一部分,例如用于指定數組類型的長度: ~~~ const IPv4Len = 4 // parseIPv4 parses an IPv4 address (d.d.d.d). func parseIPv4(s string) IP { var p [IPv4Len]byte // ... } ~~~ 一個常量的聲明也可以包含一個類型和一個值,但是如果沒有顯式指明類型,那么將從右邊的表達式推斷類型。在下面的代碼中,time.Duration是一個命名類型,底層類型是int64,time.Minute是對應類型的常量。下面聲明的兩個常量都是time.Duration類型,可以通過%T參數打印類型信息: ~~~ const noDelay time.Duration = 0 const timeout = 5 * time.Minute fmt.Printf("%T %[1]v\n", noDelay) // "time.Duration 0" fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s" fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s" ~~~ 如果是批量聲明的常量,除了第一個外其它的常量右邊的初始化表達式都可以省略,如果省略初始化表達式則表示使用前面常量的初始化表達式寫法,對應的常量類型也一樣的。例如: ~~~ const ( a = 1 b c = 2 d ) fmt.Println(a, b, c, d) // "1 1 2 2" ~~~ 如果只是簡單地復制右邊的常量表達式,其實并沒有太實用的價值。但是它可以帶來其它的特性,那就是iota常量生成器語法。 ### 3.6.1. iota 常量生成器 常量聲明可以使用iota常量生成器初始化,它用于生成一組以相似規則初始化的常量,但是不用每行都寫一遍初始化表達式。在一個const聲明語句中,在第一個聲明的常量所在的行,iota將會被置為0,然后在每一個有常量聲明的行加一。 下面是來自time包的例子,它首先定義了一個Weekday命名類型,然后為一周的每天定義了一個常量,從周日0開始。在其它編程語言中,這種類型一般被稱為枚舉類型。 ~~~ type Weekday int const ( Sunday Weekday = iota Monday Tuesday Wednesday Thursday Friday Saturday ) ~~~ 周日將對應0,周一為1,如此等等。 我們也可以在復雜的常量表達式中使用iota,下面是來自net包的例子,用于給一個無符號整數的最低5bit的每個bit指定一個名字: ~~~ type Flags uint const ( FlagUp Flags = 1 << iota // is up FlagBroadcast // supports broadcast access capability FlagLoopback // is a loopback interface FlagPointToPoint // belongs to a point-to-point link FlagMulticast // supports multicast access capability ) ~~~ 隨著iota的遞增,每個常量對應表達式1 << iota,是連續的2的冪,分別對應一個bit位置。使用這些常量可以用于測試、設置或清除對應的bit位的值: *gopl.io/ch3/netflag* ~~~ func IsUp(v Flags) bool { return v&FlagUp == FlagUp } func TurnDown(v *Flags) { *v &^= FlagUp } func SetBroadcast(v *Flags) { *v |= FlagBroadcast } func IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 } unc main() { var v Flags = FlagMulticast | FlagUp fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true" TurnDown(&v) fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false" SetBroadcast(&v) fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 false" fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true" } ~~~ 下面是一個更復雜的例子,每個常量都是1024的冪: ~~~ const ( _ = 1 << (10 * iota) KiB // 1024 MiB // 1048576 GiB // 1073741824 TiB // 1099511627776 (exceeds 1 << 32) PiB // 1125899906842624 EiB // 1152921504606846976 ZiB // 1180591620717411303424 (exceeds 1 << 64) YiB // 1208925819614629174706176 ) ~~~ 不過iota常量生成規則也有其局限性。例如,它并不能用于產生1000的冪(KB、MB等),因為Go語言并沒有計算冪的運算符。 **練習 3.13:** 編寫KB、MB的常量聲明,然后擴展到YB。 ### 3.6.2. 無類型常量 Go語言的常量有個不同尋常之處。雖然一個常量可以有任意有一個確定的基礎類型,例如int或float64,或者是類似time.Duration這樣命名的基礎類型,但是許多常量并沒有一個明確的基礎類型。編譯器為這些沒有明確的基礎類型的數字常量提供比基礎類型更高精度的算術運算;你可以認為至少有256bit的運算精度。這里有六種未明確類型的常量類型,分別是無類型的布爾型、無類型的整數、無類型的字符、無類型的浮點數、無類型的復數、無類型的字符串。 通過延遲明確常量的具體類型,無類型的常量不僅可以提供更高的運算精度,而且可以直接用于更多的表達式而不需要顯式的類型轉換。例如,例子中的ZiB和YiB的值已經超出任何Go語言中整數類型能表達的范圍,但是它們依然是合法的常量,而且可以像下面常量表達式依然有效(譯注:YiB/ZiB是在編譯期計算出來的,并且結果常量是1024,是Go語言int變量能有效表示的): ~~~ fmt.Println(YiB/ZiB) // "1024" ~~~ 另一個例子,math.Pi無類型的浮點數常量,可以直接用于任意需要浮點數或復數的地方: ~~~ var x float32 = math.Pi var y float64 = math.Pi var z complex128 = math.Pi ~~~ 如果math.Pi被確定為特定類型,比如float64,那么結果精度可能會不一樣,同時對于需要float32或complex128類型值的地方則會強制需要一個明確的類型轉換: ~~~ const Pi64 float64 = math.Pi var x float32 = float32(Pi64) var y float64 = Pi64 var z complex128 = complex128(Pi64) ~~~ 對于常量面值,不同的寫法可能會對應不同的類型。例如0、0.0、0i和'000'雖然有著相同的常量值,但是它們分別對應無類型的整數、無類型的浮點數、無類型的復數和無類型的字符等不同的常量類型。同樣,true和false也是無類型的布爾類型,字符串面值常量是無類型的字符串類型。 前面說過除法運算符/會根據操作數的類型生成對應類型的結果。因此,不同寫法的常量除法表達式可能對應不同的結果: ~~~ var f float64 = 212 fmt.Println((f - 32) * 5 / 9) // "100"; (f - 32) * 5 is a float64 fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 is an untyped integer, 0 fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float ~~~ 只有常量可以是無類型的。當一個無類型的常量被賦值給一個變量的時候,就像上面的第一行語句,或者是像其余三個語句中右邊表達式中含有明確類型的值,無類型的常量將會被隱式轉換為對應的類型,如果轉換合法的話。 ~~~ var f float64 = 3 + 0i // untyped complex -> float64 f = 2 // untyped integer -> float64 f = 1e123 // untyped floating-point -> float64 f = 'a' // untyped rune -> float64 ~~~ 上面的語句相當于: ~~~ var f float64 = float64(3 + 0i) f = float64(2) f = float64(1e123) f = float64('a') ~~~ 無論是隱式或顯式轉換,將一種類型轉換為另一種類型都要求目標可以表示原始值。對于浮點數和復數,可能會有舍入處理: ~~~ const ( deadbeef = 0xdeadbeef // untyped int with value 3735928559 a = uint32(deadbeef) // uint32 with value 3735928559 b = float32(deadbeef) // float32 with value 3735928576 (rounded up) c = float64(deadbeef) // float64 with value 3735928559 (exact) d = int32(deadbeef) // compile error: constant overflows int32 e = float64(1e309) // compile error: constant overflows float64 f = uint(-1) // compile error: constant underflows uint ) ~~~ 對于一個沒有顯式類型的變量聲明語法(包括短變量聲明語法),無類型的常量會被隱式轉為默認的變量類型,就像下面的例子: ~~~ i := 0 // untyped integer; implicit int(0) r := '\000' // untyped rune; implicit rune('\000') f := 0.0 // untyped floating-point; implicit float64(0.0) c := 0i // untyped complex; implicit complex128(0i) ~~~ 注意默認類型是規則的:無類型的整數常量默認轉換為int,對應不確定的內存大小,但是浮點數和復數常量則默認轉換為float64和complex128。Go語言本身并沒有不確定內存大小的浮點數和復數類型,而且如果不知道浮點數類型的話將很難寫出正確的數值算法。 如果要給變量一個不同的類型,我們必須顯式地將無類型的常量轉化為所需的類型,或給聲明的變量指定明確的類型,像下面例子這樣: ~~~ var i = int8(0) var i int8 = 0 ~~~ 當嘗試將這些無類型的常量轉為一個接口值時(見第7章),這些默認類型將顯得尤為重要,因為要靠它們明確接口對應的動態類型。 ~~~ fmt.Printf("%T\n", 0) // "int" fmt.Printf("%T\n", 0.0) // "float64" fmt.Printf("%T\n", 0i) // "complex128" fmt.Printf("%T\n", '\000') // "int32" (rune) ~~~ 現在我們已經講述了Go語言中全部的基礎數據類型。下一步將演示如何用基礎數據類型組合成數組或結構體等復雜數據類型,然后構建用于解決實際編程問題的數據結構,這將是第四章的討論主題。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看