Go 基礎之基本資料型別

賈維斯Echo發表於2023-10-07

Go 基礎之基本資料型別

一、整型

所謂整型,主要用來表示現實世界中整型數量,比如年齡,分數,排名等等

Go 語言整型可以分為平臺無關整型平臺相關整型這兩種,它們的區別主要就在,這些整數型別在不同 CPU 架構或作業系統下面,它們的長度是否是一致的。

1.1 平臺無關整型

1.1.1 基本概念

  • Go語言提供了幾種平臺無關的整數型別,它們的長度在不同的平臺上是一致的。

1.1.2 分類

平臺無關的整型也可以分成兩類:

有符號整型(int8~int64)

使用最高位(最左邊的位)作為符號位,表示正數和負數。有符號整型的取值範圍是從負數到正數,因此可以表示負數、零和正數。

無符號整型(uint8~uint64)

不使用符號位,因此只能表示非負數(零和正數)。無符號整型的取值範圍是從零到正數最大值。

型別 長度 取值範圍
有符號整型 int8 1個位元組 [-128, 127]
int16 2個位元組 [-32768, 32767]
int32 4個位元組 [- 2147483648, 2147483647]
int64 8個位元組 [ -9223372036854775808, 9223372036854775807]
無符號整型 uint8 1個位元組 [0, 255]
uint16 2個位元組 [0, 65535]
uint32 4個位元組 [0, 4294967295]
uint64 8個位元組 [0, 18446744073709551615]

有符號整型(int8int64)和無符號整型(uint8uint64)兩者的本質差別在於最高二進位制位(bit 位)是否被解釋為符號位,這點會影響到無符號整型與有符號整型的取值範圍

以下圖中的這個 8 位元(一個位元組)的整型值為例,當它被解釋為無符號整型 uint8 時,和它被解釋為有符號整型 int8 時表示的值是不同的:

img

在同樣的位元位表示下,當最高位元位被解釋為符號位時,它代表一個有符號整型(int8),它表示的值為 -127;當最高位元位不被解釋為符號位時,它代表一個無符號整型 (uint8),它表示的值為 129。

即便最高位元位被解釋為符號位,上面的有符號整型所表示值也應該為 -1 啊,怎麼會是 -127 呢?

這是因為 Go 採用 2 的補碼(Two’s Complement)作為整型的位元位編碼方法。因此,我們不能簡單地將最高位元位看成負號,把其餘位元位表示的值看成負號後面的數值。Go 的補碼是透過原碼逐位取反後再加 1 得到的,比如,我們以 -127 這個值為例,它的補碼轉換過程就是這樣的:

WechatIMG172

1.2 平臺相關整型

1.2.1 基本概念

  • Go語言還提供了一些整數型別,它們的長度會根據執行平臺的改變而改變,這些整數型別的長度依賴於具體的CPU架構和作業系統。
型別 32位長度 64位長度
預設的有符號整型 int 32位(4位元組) 64位(8位元組)
預設的無符號整型 unit 32位(4位元組) 64位(8位元組)
無符號整型 uintptr """大到足以儲存任意一個指標的值""(Go規範描 述)"

1.2.2 注意點

在這裡我們要特別注意一點,由於這三個型別的長度是平臺相關的,所以我們在編寫有移植性要求的程式碼時,千萬不要強依賴這些型別的長度

1.2.3 獲取三個型別在目標執行平臺上的長度

如果你不知道這三個型別在目標執行平臺上的長度,可以透過 unsafe 包提供的 SizeOf 函式來獲取

比如在 x86-64 平臺上,它們的長度均為 8:

var a, b = int(5), uint(6)
var p uintptr = 0x12345678
fmt.Println("signed integer a's length is", unsafe.Sizeof(a)) // 8
fmt.Println("unsigned integer b's length is", unsafe.Sizeof(b)) // 8
fmt.Println("uintptr's length is", unsafe.Sizeof(p)) // 8

1.3 整型的溢位問題

1.3.1 什麼是整形溢位?

  • 整型溢位指的是在整型變數所能表示的數值範圍之外的值。整型變數通常有最大值和最小值限制

無論哪種整型,都有它的取值範圍,也就是有它可以表示的值邊界。如果這個整型因為參與某個運算,導致結果超出了這個整型的值邊界,我們就說發生了整型溢位的問題。由於整型無法表示它溢位後的那個“結果”,所以出現溢位情況後,對應的整型變數的值依然會落到它的取值範圍內,只是結果值與我們的預期不符,導致程式邏輯出錯。

var s int8 = 127
s += 1 // 預期128,實際結果-128

var u uint8 = 1
u -= 2 // 預期-1,實際結果255

你看,有符號整型變數 s 初始值為 127,在加 1 操作後,我們預期得到 128,但由於 128 超出了 int8 的取值邊界,其實際結果變成了 -128。無符號整型變數 u 也是一樣的道理,它的初值為 1,在進行減 2 操作後,我們預期得到 -1,但由於 -1 超出了 uint8 的取值邊界,它的實際結果變成了 255。

這個問題最容易發生在迴圈語句的結束條件判斷中,因為這也是經常使用整型變數的地方。無論無符號整型,還是有符號整型都存在溢位的問題,所以我們要十分小心地選擇參與迴圈語句結束判斷的整型變數型別,以及與之比較的邊界值。

1.4 整型字面值與格式化輸出

Go 語言在設計開始,就繼承了 C 語言關於數值字面值(Number Literal)的語法形式。早期 Go 版本支援十進位制、八進位制、十六進位制的數值字面值形式,比如:

a := 53        // 十進位制
b := 0700      // 八進位制,以"0"為字首
c1 := 0xaabbcc // 十六進位制,以"0x"為字首
c2 := 0Xddeeff // 十六進位制,以"0X"為字首

Go 1.13 版本中,Go 又增加了對二進位制字面值的支援和兩種八進位制字面值的形式,比如:

d1 := 0b10000001 // 二進位制,以"0b"為字首
d2 := 0B10000001 // 二進位制,以"0B"為字首
e1 := 0o700      // 八進位制,以"0o"為字首
e2 := 0O700      // 八進位制,以"0O"為字首

為提升字面值的可讀性,Go 1.13 版本還支援在字面值中增加數字分隔符“_”,分隔符可以用來將數字分組以提高可讀性。比如每 3 個數字一組,也可以用來分隔字首與字面值中的第一個數字:

a := 5_3_7   // 十進位制: 537
b := 0b_1000_0111  // 二進位制位表示為10000111 
c1 := 0_700  // 八進位制: 0700
c2 := 0o_700 // 八進位制: 0700
d1 := 0x_5c_6d // 十六進位制:0x5c6d

不過,這裡你要注意一下,Go 1.13 中增加的二進位制字面值以及數字分隔符,只在 go.mod 中的 go version 指示欄位為 Go 1.13 以及以後版本的時候,才會生效,否則編譯器會報錯。

反過來,我們也可以透過標準庫 fmt 包的格式化輸出函式,將一個整型變數輸出為不同進位制的形式。比如下面就是將十進位制整型值 59,格式化輸出為二進位制、八進位制和十六進位制的程式碼:

var a int8 = 59
fmt.Printf("%b\n", a) //輸出二進位制:111011
fmt.Printf("%d\n", a) //輸出十進位制:59
fmt.Printf("%o\n", a) //輸出八進位制:73
fmt.Printf("%O\n", a) //輸出八進位制(帶0o字首):0o73
fmt.Printf("%x\n", a) //輸出十六進位制(小寫):3b
fmt.Printf("%X\n", a) //輸出十六進位制(大寫):3B

二、浮點型

2.1 IEEE 754 標準

IEEE 754 是 IEEE 制定的二進位制浮點數算術標準,它是 20 世紀 80 年代以來最廣泛使用的浮點數運算標準,被許多 CPU 與浮點運算器採用。現存的大部分主流程式語言,包括 Go 語言,都提供了符合 IEEE 754 標準的浮點數格式與算術運算。

IEEE 754 標準規定了四種表示浮點數值的方式:單精度(32 位)、雙精度(64 位)、擴充套件單精度(43 位元以上)與擴充套件雙精度(79 位元以上,通常以 80 位實現)。後兩種其實很少使用,我們重點關注前面兩個就好了。

2.2 浮點型別

Go語言支援兩種浮點型數:float32float64。這兩種浮點型資料格式遵循IEEE 754標準

浮點型別與前面的整型相比,Go 提供的浮點型別都是平臺無關的。

2.3 float32float64 這兩種浮點型別有什麼異同點呢?

無論是 float32 還是 float64,它們的變數的預設值都為 0.0,不同的是它們佔用的記憶體空間大小是不一樣的,可以表示的浮點數的範圍與精度也不同。那麼浮點數在記憶體中的二進位制表示究竟是怎麼樣的呢?

浮點數在記憶體中的二進位制表示(Bit Representation)要比整型複雜得多,IEEE 754 規範給出了在記憶體中儲存和表示一個浮點數的標準形式,見下圖:

WechatIMG173

我們看到浮點數在記憶體中的二進位制表示分三個部分:符號位、階碼(即經過換算的指數),以及尾數。這樣表示的一個浮點數,它的值等於:

img

其中浮點值的符號由符號位決定:當符號位為 1 時,浮點值為負值;當符號位為 0 時,浮點值為正值。公式中 offset 被稱為階碼偏移值,這個我們待會再講。

我們首先來看單精度(float32)與雙精度(float64)浮點數在階碼和尾數上的不同。這兩種浮點數的階碼與尾數所使用的位數是不一樣的,你可以看下 IEEE 754 標準中單精度和雙精度浮點數的各個部分的長度規定:

WechatIMG174

我們看到,單精度浮點型別(float32)為符號位分配了 1 個 bit,為階碼分配了 8 個 bit,剩下的 23 個 bit 分給了尾數。而雙精度浮點型別,除了符號位的長度與單精度一樣之外,其餘兩個部分的長度都要遠大於單精度浮點型,階碼可用的 bit 位數量為 11,尾數則更是擁有了 52 個 bit 位。

接著,我們再來看前面提到的“階碼偏移值”,我想用一個例子直觀地讓你感受一下。在這個例子中,我們來看看如何將一個十進位制形式的浮點值 139.8125,轉換為 IEEE 754 規定中的那種單精度二進位制表示。

步驟一:我們要把這個浮點數值的整數部分和小數 部分,分別轉換為二進位制形式(字尾 d 表示十進位制數,字尾 b 表示二進位制數):

  • 整數部分:139d => 10001011b;
  • 小數部分:0.8125d => 0.1101b(十進位制小數轉換為二進位制可採用“乘 2 取整”的豎式計算)。

這樣,原浮點值 139.8125d 進行二進位制轉換後,就變成 10001011.1101b。

步驟二:移動小數點,直到整數部分僅有一個 1,也就是 10001011.1101b => 1.00010111101b。我們看到,為了整數部分僅保留一個 1,小數點向左移了 7 位,這樣指數就為 7,尾數為 00010111101b。

步驟三:計算階碼。

IEEE754 規定不能將小數點移動得到的指數,直接填到階碼部分,指數到階碼還需要一個轉換過程。對於 float32 的單精度浮點數而言,階碼 = 指數 + 偏移值。偏移值的計算公式為 2^(e-1)-1,其中 e 為階碼部分的 bit 位數,這裡為 8,於是單精度浮點數的階碼偏移值就為 2^(8-1)-1 = 127。這樣在這個例子中,階碼 = 7 + 127 = 134d = 10000110b。float64 的雙精度浮點數的階碼計算也是這樣的。

步驟四:將符號位、階碼和尾數填到各自位置,得到最終浮點數的二進位制表示。尾數位數不足 23 位,可在後面補 0。

WechatIMG175

這樣,最終浮點數 139.8125d 的二進位制表示就為 0b_0_10000110_00010111101_000000000000

最後,我們再透過 Go 程式碼輸出浮點數 139.8125d 的二進位制表示,和前面我們手工轉換的做一下比對,看是否一致。

func main() {
    var f float32 = 139.8125
    bits := math.Float32bits(f)
    fmt.Printf("%b\n", bits)
}

在這段程式碼中,我們透過標準庫的 math 包,將 float32 轉換為整型。在這種轉換過程中,float32 的記憶體表示是不會被改變的。然後我們再透過前面提過的整型值的格式化輸出,將它以二進位制形式輸出出來。執行這個程式,我們得到下面的結果:

1000011000010111101000000000000

我們看到這個值在填上省去的最高位的 0 後,與我們手工得到的浮點數的二進位制表示一模一樣。這就說明我們手工推導的思路並沒有錯。

而且,你可以從這個例子中感受到,階碼和尾數的長度決定了浮點型別可以表示的浮點數範圍與精度。因為雙精度浮點型別(float64)階碼與尾數使用的位元位數更多,它可以表示的精度要遠超單精度浮點型別,所以在日常開發中,我們使用雙精度浮點型別(float64)的情況更多,這也是 Go 語言中浮點常量或字面值的預設型別。

而 float32 由於表示範圍與精度有限,經常會給開發者造成一些困擾。比如我們可能會因為 float32 精度不足,導致輸出結果與常識不符。比如下面這個例子就是這樣,f1 與 f2 兩個浮點型別變數被兩個不同的浮點字面值初始化,但邏輯比較的結果卻是兩個變數的值相等。至於其中原因,我將作為思考題留給你,你可以結合前面講解的浮點型別表示方法,對這個例子進行分析:

var f1 float32 = 16777216.0
var f2 float32 = 16777217.0
fmt.Println(f1 == f2) // true

看到這裡,你是不是覺得浮點型別很神奇?和易用易理解的整型相比,浮點型別無論在二進位制表示層面,還是在使用層面都要複雜得多。

2.4 浮點型字面值與格式化輸出

2.4.1 直白地用十進位制表示的浮點值形式

這一類,我們透過字面值就可直接確定它的浮點值,比如:

3.1415
.15  // 整數部分如果為0,整數部分可以省略不寫
81.80
82. // 小數部分如果為0,小數點後的0可以省略不寫

2.4.2 科學計數法形式

採用科學計數法表示的浮點字面值,我們需要透過一定的換算才能確定其浮點值。而且在這裡,科學計數法形式又分為十進位制形式表示的,和十六進位制形式表示的兩種

使用十進位制科學計數法形式的浮點數字面值,這裡字面值中的 e/E 代表的冪運算的底數為 10:

6674.28e-2 // 6674.28 * 10^(-2) = 66.742800
.12345E+5  // 0.12345 * 10^5 = 12345.000000

接著是十六進位制科學計數法形式的浮點數:

0x2.p10  // 2.0 * 2^10 = 2048.000000
0x1.Fp+0 // 1.9375 * 2^0 = 1.937500

這裡,我們要注意,十六進位制科學計數法的整數部分、小數部分用的都是十六進位制形式,但指數部分依然是十進位制形式,並且字面值中的 p/P 代表的冪運算的底數為 2。

2.4.3 浮點數的格式化輸出

知道了浮點型的字面值後,和整型一樣,fmt 包也提供了針對浮點數的格式化輸出。我們最常使用的格式化輸出形式是 %f。透過 %f,我們可以輸出浮點數最直觀的原值形式。

var f float64 = 123.45678
fmt.Printf("%f\n", f) // 123.456780

我們也可以將浮點數輸出為科學計數法形式,如下面程式碼:

fmt.Printf("%e\n", f) // 1.234568e+02
fmt.Printf("%x\n", f) // 0x1.edd3be22e5de1p+06

其中 %e 輸出的是十進位制的科學計數法形式,而 %x 輸出的則是十六進位制的科學計數法形式

2.5 math 包

math 包是Go語言標準庫中的一個核心包,它提供了各種數學函式和常量,用於進行各種數學操作。

2.6 數字型別的極值

在Go語言中,數字型別的極值常量通常儲存在math包中。這些常量用於表示浮點數和整數型別的最大值和最小值,以及其他數學常量。

  • intuint 族都有最大值和最小值。
  • float32float64 只有最大值和最小正數, 沒有最小值。

以下是一些常見的math包中的數字型別極值常量:

  1. 整數型別的極值
    • math.MaxInt8: int8 型別的最大值。
    • math.MinInt8: int8 型別的最小值。
    • math.MaxInt16: int16 型別的最大值。
    • math.MinInt16: int16 型別的最小值。
    • math.MaxInt32: int32 型別的最大值。
    • math.MinInt32: int32 型別的最小值。
    • math.MaxInt64: int64 型別的最大值。
    • math.MinInt64: int64 型別的最小值。
  2. 浮點數型別的極值
    • math.MaxFloat32: float32 型別的最大正有限值。
    • math.SmallestNonzeroFloat32: float32 型別的最小正非零值。
    • math.MaxFloat64: float64 型別的最大正有限值。
    • math.SmallestNonzeroFloat64: float64 型別的最小正非零值。
  3. 其他數學常量
    • math.Pi: 圓周率 π 的近似值。
    • math.E: 自然對數的底數 e 的近似值。
    • math.Sqrt2math.SqrtE:平方根的近似值。

這些常量可以在你的Go程式中使用,以便在演算法和數學計算中引用數字型別的極值和常量。例如,你可以使用 math.MaxInt64 來表示 int64 型別的最大值,以便進行整數計算時進行比較或限制數值範圍。同樣,math.Pi 可用於計算圓的周長或面積等數學運算。

package main

import (
	"fmt"
	"math"
)

func main() {
	// 整數型別的極值
	fmt.Println("int8 Max:", math.MaxInt8)
	fmt.Println("int8 Min:", math.MinInt8)
	fmt.Println("int16 Max:", math.MaxInt16)
	fmt.Println("int16 Min:", math.MinInt16)
	fmt.Println("int32 Max:", math.MaxInt32)
	fmt.Println("int32 Min:", math.MinInt32)
	fmt.Println("int64 Max:", math.MaxInt64)
	fmt.Println("int64 Min:", math.MinInt64)

	// 浮點數型別的極值
	fmt.Println("float32 Max:", math.MaxFloat32)
	fmt.Println("float32 Smallest Non-zero:", math.SmallestNonzeroFloat32)
	fmt.Println("float64 Max:", math.MaxFloat64)
	fmt.Println("float64 Smallest Non-zero:", math.SmallestNonzeroFloat64)

	// 其他數學常量
	fmt.Println("Pi:", math.Pi)
	fmt.Println("E:", math.E)
	fmt.Println("Sqrt(2):", math.Sqrt2)
	fmt.Println("Sqrt(E):", math.SqrtE)
}

三、小結

  • int 家族:int8int16int32int64int。在記憶體中分別用 1、2、4、8 個位元組來表達,而 int 用幾個位元組則取決於CPU。目前大多數 int 都是 8 位元組。 如果猶豫不決,就優先使用int,特殊情況除外

  • uint 家族:uint8uint16uint32uint64 uint。無符號整數,類似於 int 家族,優先使用 uint

  • float 家族:float32、float64。浮點數,優先使用 float64

四、複數

4.1 複數

  • 數學課本上將形如 z=a+bi(a、b 均為實數,a 稱為實部,b 稱為虛部)的數稱為複數

  • Go 語言則原生支援複數型別。

  • 和整型、浮點型相比,複數型別在 Go 中的應用就更為侷限和小眾,主要用於專業領域的計算,比如向量計算等。我們簡單瞭解一下就可以了。

4.2 複數型別

  • Go 提供兩種複數型別,它們分別是 complex64complex128

  • complex64 的實部與虛部都是 float32 型別,而 complex128 的實部與虛部都是 float64 型別。

  • 如果一個複數沒有顯示賦予型別,那麼它的預設型別為 complex128

4.3 複數字面值

關於複數字面值的表示,我們其實有三種方法。

第一種,我們可以透過複數字面值直接初始化一個複數型別變數:

var c = 5 + 6i
var d = 0o123 + .12345E+5i // 83+12345i

第二種,Go 還提供了 complex 函式,方便我們建立一個 complex128 型別值:

var c = complex(5, 6) // 5 + 6i
var d = complex(0o123, .12345E+5) // 83+12345i

第三種,你還可以透過 Go 提供的預定義的函式 real 和 imag,來獲取一個複數的實部與虛部,返回值為一個浮點型別:

var c = complex(5, 6) // 5 + 6i
r := real(c) // 5.000000
i := imag(c) // 6.000000

4.4 格式化輸出

複數形式的格式化輸出的問題,由於 complex 型別的實部與虛部都是浮點型別,所以我們可以直接運用浮點型的格式化輸出方法,來輸出複數型別,以下是一個示例:

package main

import "fmt"

func main() {
    // 建立複數
    z := complex(3, 4) // 3 + 4i

    // 獲取複數的實部和虛部,並使用浮點型格式化輸出
    realPart := real(z)
    imagPart := imag(z)

    fmt.Printf("Real Part: %f\n", realPart)
    fmt.Printf("Imaginary Part: %f\n", imagPart)
}

五、布林值

5.1 布林型別宣告

在Go語言中,可以使用bool關鍵字來宣告布林型別變數,這些變數只能取 true(真)或 false(假)這兩個值。

var isTrue bool = true
var isFalse bool = false

5.2 注意:

  1. 預設值:布林型別變數的預設值為false

    	var a bool
    	fmt.Println(a) // 輸出為false
    
  2. 強制型別轉換:Go語言不允許將整數或其他資料型別強制轉換為布林型。只有布林型別的值可以被用於布林表示式中,比如條件語句。

    	var x string = "2222"
    	// 錯誤,cannot convert x (variable of type string) to type bool
    	var isTrue bool = bool(x)
    	fmt.Println(isTrue)
    
  3. 數值運算和轉換:布林型無法直接參與數值運算,也無法與其他資料型別進行數值轉換。這是因為布林型別只有兩個可能的值,不適合數學運算或數值轉換。

    var b bool = true
    var i int = 10
    
    // 錯誤,無法將布林型和整數相加
    // var result = b + i
    
    // 錯誤,無法將整數轉換為布林型
    // var b2 bool = bool(i)
    

5.3 邏輯運算子

運算子 說明 示例 結果
! (非) 取反,將真變假,假變真 !true false
&& (與) 兩邊都為真,結果為真,否則為假 true && false false
|| (或) 兩邊任意一邊為真,結果為真,兩邊都為假,結果為假 true || false true
== (等於) 兩邊相等,結果為真;不相等,結果為假 1 == 1 true
!= (不等於) 兩邊不相等,結果為真;相等,結果為假 1 != 2 true
package main

import "fmt"

func main() {
	var isTrue bool = true
	var isFalse bool = false
	// 布林值的邏輯運算
	andResult := isTrue && isFalse // 與運算,結果為 false
	orResult := isTrue || isFalse  // 或運算,結果為 true
	notResult := !isTrue           // 非運算,結果為 false

	fmt.Println("AND result:", andResult)
	fmt.Println("OR result:", orResult)
	fmt.Println("NOT result:", notResult)
}

六、字串

6.1 兩種寫法

6.1.1 單行字串

使用雙引號" "來定義:

s1 := "This is a string"

6.1.2 多行字串

使用反引號 `` 來定義:

var s string = `         ,_---~~~~~----._
    _,,_,*^____      _____*g*\"*,--,
   / __/ /'     ^.  /      \ ^@q   f
  [  @f | @))    |  | @))   l  0 _/
   \/   \~____ / __ \_____/     \
    |           _l__l_           I
    }          [______]           I
    ]            | | |            |
    ]             ~ ~             |
    |                            |
     |                           |`
fmt.Println(s)

多行字串中可以直接加入換行,並且會原樣保留換行符。

多行字串常用於需要編寫包含換行的長文字。

需要注意:

  • 單雙引號定義的字串效果一樣
  • 多行字串會保留文字中的空格和換行
  • 不能在單行字串內直接換行,需要使用轉義符\n

6.2 字串轉義符

Go 語言的字串常見轉義符包含回車、換行、單雙引號、製表符等,如下表所示。

轉義符 含義
\r 回車符(返回行首)
\n 換行符(直接跳到下一行的同列位置)
\t 製表符
\' 單引號
\" 雙引號
\\ 反斜槓

舉個例子,我們要列印一個Windows平臺下的一個檔案路徑:

package main
import (
    "fmt"
)
func main() {
    fmt.Println("str := \"c:\\Code\\lesson1\\go.exe\"")
}

6.3 字串特性

6.3.1 string 型別的資料是不可變的

  • string 型別的資料是不可變的,提高了字串的併發安全性和儲存利用率。

Go 語言規定,字串型別的值在它的生命週期內是不可改變的。這就是說,如果我們宣告瞭一個字串型別的變數,那我們是無法透過這個變數改變它對應的字串值的,但這並不是說我們不能為一個字串型別變數進行二次賦值。

什麼意思呢?我們看看下面的程式碼就好理解了:

var s string = "hello"
s[0] = 'k'   // 錯誤:字串的內容是不可改變的
s = "gopher" // ok

在這段程式碼中,我們宣告瞭一個字串型別變數 s。當我們試圖透過下標方式把這個字串的第一個字元由 h 改為 k 的時候,我們會收到編譯器錯誤的提示:字串是不可變的。但我們仍可以像最後一行程式碼那樣,為變數 s 重新賦值為另外一個字串。

Go 這樣的“字串型別資料不可變”的性質給開發人員帶來的最大好處,就是我們不用再擔心字串的併發安全問題。這樣,Go 字串可以被多個 Goroutine(Go 語言的輕量級使用者執行緒)共享,開發者不用因為擔心併發安全問題,使用會帶來一定開銷的同步機制。

另外,也由於字串的不可變性,針對同一個字串值,無論它在程式的幾個位置被使用,Go 編譯器只需要為它分配一塊儲存就好了,大大提高了儲存利用率。

6.3.2 Go 語言裡的字串的內部實現使用UTF-8編碼

  • 對非 ASCII 字元提供原生支援,消除了原始碼在不同環境下顯示亂碼的可能。

Go 語言原始檔預設採用的是 Unicode 字符集,Unicode 字符集是目前市面上最流行的字符集,它囊括了幾乎所有主流非 ASCII 字元(包括中文字元)。Go 字串中的每個字元都是一個 Unicode 字元,並且這些 Unicode 字元是以 UTF-8 編碼格式儲存在記憶體當中的。

6.4 字串的常用操作

6.4.1 字串拼接

  • 使用+運算子拼接字串

    	s := "hello" + " world" 
    	println(s)
    
  • 透過fmt.Sprintf進行格式化拼接

    println(fmt.Sprintf("hello %d", 123))
    

6.4.2 字串長度

  • 使用len() 獲取字串位元組長度:

    	println(len("你好")) // 輸出6
    	println(len("你好abc")) // 輸出9
    
  • 使用utf8.RuneCountInString()函式獲取字元個數:

    println(utf8.RuneCountInString("你好abc")) // 輸出5
    

6.4.3 索引取值

在字串的實現中,真正儲存資料的是底層的陣列。字串的下標操作本質上等價於底層陣列的下標操作。可以透過索引下標訪問字串中每個字元:

var s = "中國人"
fmt.Printf("0x%x\n", s[0]) // 0xe4:字元“中” utf-8編碼的第一個位元組

我們可以看到,透過下標操作,我們獲取的是字串中特定下標上的位元組,而不是字元。

6.4.4 字元迭代

Go 有兩種迭代形式:常規 for 迭代與 for range 迭代。你要注意,通過這兩種形式的迭代對字串進行操作得到的結果是不同的。

(一)、for 迭代

透過常規 for 迭代對字串進行的操作是一種位元組視角的迭代,每輪迭代得到的的結果都是組成字串內容的一個位元組,以及該位元組所在的下標值,這也等價於對字串底層陣列的迭代,比如下面程式碼:

var s = "中國人"

for i := 0; i < len(s); i++ {
  fmt.Printf("index: %d, value: 0x%x\n", i, s[i])
}

執行這段程式碼,我們會看到,經過常規 for 迭代後,我們獲取到的是字串裡字元的 UTF-8 編碼中的一個位元組:

index: 0, value: 0xe4
index: 1, value: 0xb8
index: 2, value: 0xad
index: 3, value: 0xe5
index: 4, value: 0x9b
index: 5, value: 0xbd
index: 6, value: 0xe4
index: 7, value: 0xba
index: 8, value: 0xba
(二)、for range 迭代

像下面這樣使用 for range 迭代,我們得到的又是什麼呢?我們繼續看程式碼:

var s = "中國人"

for i, v := range s {
    fmt.Printf("index: %d, value: 0x%x\n", i, v)
}

同樣執行一下這段程式碼,我們得到:

index: 0, value: 0x4e2d
index: 3, value: 0x56fd
index: 6, value: 0x4eba

我們看到,透過 for range 迭代,我們每輪迭代得到的是字串中 Unicode 字元的碼點值,以及該字元在字串中的偏移值。我們可以透過這樣的迭代,獲取字串中的字元個數,而透過 Go 提供的內建函式 len,我們只能獲取字串內容的長度(位元組個數)。當然了,獲取字串中字元個數更專業的方法,是呼叫標準庫 UTF-8 包中的 RuneCountInString 函式。

6.4.5 字串比較

Go 字串型別支援各種比較關係運算子,包括 = =、!= 、>=、<=、> 和 <。在字串的比較上,Go 採用字典序的比較策略,分別從每個字串的起始處,開始逐個位元組地對兩個字串型別變數進行比較。

當兩個字串之間出現了第一個不相同的元素,比較就結束了,這兩個元素的比較結果就會做為串最終的比較結果。如果出現兩個字串長度不同的情況,長度比較小的字串會用空元素補齊,空元素比其他非空元素都小。

以下是Go 字串比較的示例:

func main() {
        // ==
        s1 := "世界和平"
        s2 := "世界" + "和平"
        fmt.Println(s1 == s2) // true

        // !=
        s1 = "Go"
        s2 = "C"
        fmt.Println(s1 != s2) // true

        // < and <=
        s1 = "12345"
        s2 = "23456"
        fmt.Println(s1 < s2)  // true
        fmt.Println(s1 <= s2) // true

        // > and >=
        s1 = "12345"
        s2 = "123"
        fmt.Println(s1 > s2)  // true
        fmt.Println(s1 >= s2) // true
}

你可以看到,鑑於 Go string 型別是不可變的,所以說如果兩個字串的長度不相同,那麼我們不需要比較具體字串資料,也可以斷定兩個字串是不同的。但是如果兩個字串長度相同,就要進一步判斷,資料指標是否指向同一塊底層儲存資料。如果還相同,那麼我們可以說兩個字串是等價的,如果不同,那就還需要進一步去比對實際的資料內容。

6.4.6 字串轉換

在這方面,Go 支援字串與位元組切片、字串與 rune 切片的雙向轉換,並且這種轉換無需呼叫任何函式,只需使用顯式型別轉換就可以了。我們看看下面程式碼:

var s string = "中國人"
                      
// string -> []rune
rs := []rune(s) 
fmt.Printf("%x\n", rs) // [4e2d 56fd 4eba]
                
// string -> []byte
bs := []byte(s) 
fmt.Printf("%x\n", bs) // e4b8ade59bbde4baba
                
// []rune -> string
s1 := string(rs)
fmt.Println(s1) // 中國人
                
// []byte -> string
s2 := string(bs)
fmt.Println(s2) // 中國人

6.4.7 strings 提供的常用API

strings包提供了Go語言字串操作的常用API:

函式 說明
strings.Contains(s, substr) 判斷字串s是否包含子串substr
strings.Index(s, substr) 返回子串substr在s中首次出現的索引
strings.Join(a[]string, sep) 將字串陣列a透過sep連線起來
strings.Repeat(s, count) 重複s字串count次
strings.Replace(s, old, new, n) 將s中的old子串替換為new,最多n次
strings.Split(s, sep) 透過sep分割s字串,返回子串陣列
strings.ToLower(s) s全部轉為小寫
strings.ToUpper(s) s全部轉為大寫
strings.Fields(s) 將字串s按空格分解為子串陣列
strings.TrimSpace(s) 去除s頭尾空白字元

七、byte和rune型別

7.1 字元

組成每個字串的元素叫做“字元”,可以透過遍歷或者單個獲取字串元素獲得字元。 字元用單引號(’)包裹起來,如:

var a = '中'
var b = 'x'

7.2 byte

  • byte型別代表了ASCII碼的一個字元。位元組的值範圍是0到255。是 uint8 的別名。
  • byte型別經常用於處理二進位制資料流,或需要處理ASCII字元時,如讀寫檔案,資料流的編碼解碼等。

例如:

var a byte = 'A'

7.3 rune

  • runeint32 的別名,它用於表示32位的Unicode字元,範圍從0到65535,。
  • rune型別代表一個UTF-8字元。實際上它是一個int32的別名。
  • rune 型別通常用於處理文字和字串,特別是當需要支援多語言字符集(如UTF-8編碼的Unicode字元)時。比如:需要處理中文、日文或者其他複合字元時,則需要用到rune型別。

例如:

var a rune = '國'

7.4 簡單示例

以下是一些示例,演示了byterune 型別的用法:

package main

import "fmt"

func main() {
    // 使用 byte 表示單個 ASCII 字元
    var ch byte = 'A'
    fmt.Printf("byte: %c\n", ch)

    // 使用 rune 表示 Unicode 字元
    var ru rune = '你'
    fmt.Printf("rune: %c\n", ru)

    // 使用 rune 處理字串中的 Unicode 字元
    str := "你好,世界!"
    for _, char := range str {
        fmt.Printf("rune: %c\n", char)
    }
}

在上述示例中,我們首先使用 byterune 分別表示單個ASCII字元和Unicode字元。然後,我們使用 rune 處理包含多個Unicode字元的字串。

總之,byterune 型別在Go語言中用於處理字元和位元組,其中 byte 主要用於處理ASCII字元和位元組資料,而 rune 用於處理Unicode字元,特別是用於處理多語言文字。

八、型別轉換

Go語言中只有強制型別轉換,沒有隱式型別轉換。該語法只能在兩個型別之間支援相互轉換的時候使用。

強制型別轉換的基本語法如下:

T(expression)

其中,T 表示要轉換的目標型別,expression 是要轉換的表示式。這個語法只能在兩種資料型別之間支援相互轉換的情況下使用。

以下是一些示例,演示了不同資料型別之間的強制型別轉換:

package main

import "fmt"

func main() {
    // 整數到浮點數的轉換
    var x int = 42
    y := float64(x)
    fmt.Printf("x: %d, y: %f\n", x, y)

    // 浮點數到整數的轉換(截斷小數部分)
    var a float64 = 3.14
    b := int(a)
    fmt.Printf("a: %f, b: %d\n", a, b)

    // 型別不匹配,需要顯式轉換
    var c int = 10
    var d int64 = int64(c)
    fmt.Printf("c: %d, d: %d\n", c, d)

    // 字串到位元組切片的轉換
    str := "Hello, Go!"
    bytes := []byte(str)
    fmt.Printf("str: %s, bytes: %v\n", str, bytes)
}

要注意的是,強制型別轉換必須在相互相容的型別之間進行,且需要明確指定目標型別,不會自動進行隱式轉換。

九、自定義型別

9.1 type 關鍵字

如果我們要透過 Go 提供的型別定義語法,來建立自定義的數值型別,我們可以透過 type 關鍵字基於原生內建型別來宣告一個新型別。

9.2 自定義型別

下面我們就來建立一個名為 MyInt 的新的數值型別看看:

type MyInt int32

這裡,因為 MyInt 型別的底層型別是 int32,所以它的數值性質與 int32 完全相同,但它們仍然是完全不同的兩種型別。根據 Go 的型別安全規則,我們無法直接讓它們相互賦值,或者是把它們放在同一個運算中直接計算,這樣編譯器就會報錯。

var m int = 5
var n int32 = 6
var a MyInt = m // 錯誤:在賦值中不能將m(int型別)作為MyInt型別使用
var a MyInt = n // 錯誤:在賦值中不能將n(int32型別)作為MyInt型別使用

要避免這個錯誤,我們需要藉助顯式轉型,讓賦值運算子左右兩邊的運算元保持型別一致,像下面程式碼中這樣做:

var m int = 5
var n int32 = 6
var a MyInt = MyInt(m) // ok
var a MyInt = MyInt(n) // ok

我們也可以透過 Go 提供的型別別名(Type Alias)語法來自定義數值型別。和上面使用標準 type 語法的定義不同的是,透過型別別名語法定義的新型別與原型別別無二致,可以完全相互替代。我們來看下面程式碼:

type MyInt = int32

var n int32 = 6
var a MyInt = n // ok

你可以看到,透過型別別名定義的 MyInt 與 int32 完全等價,所以這個時候兩種型別就是同一種型別,不再需要顯式轉型,就可以相互賦值。

十、數字型別基本運算

10.1 數字型別的基本運算子表格

運算子 描述 示例
+ 加法 x + y
- 減法 x - y
* 乘法 x * y
/ 除法 x / y
% 求餘 x % y
++ 自增 x++
-- 自減 x--

10.2 基本運算

在Go語言中,數字型別(包括整數和浮點數)支援基本的數學運算,包括加法、減法、乘法和除法。以下是這些運算的示例:

package main

import "fmt"

func main() {
    // 整數運算
    var a, b int = 10, 5

    // 加法
    sum := a + b
    fmt.Printf("a + b = %d\n", sum)

    // 減法
    difference := a - b
    fmt.Printf("a - b = %d\n", difference)

    // 乘法
    product := a * b
    fmt.Printf("a * b = %d\n", product)

    // 除法
    quotient := a / b
    fmt.Printf("a / b = %d\n", quotient)

    // 浮點數運算
    var x, y float64 = 3.14, 1.5

    // 加法
    sumFloat := x + y
    fmt.Printf("x + y = %f\n", sumFloat)

    // 減法
    differenceFloat := x - y
    fmt.Printf("x - y = %f\n", differenceFloat)

    // 乘法
    productFloat := x * y
    fmt.Printf("x * y = %f\n", productFloat)

    // 除法
    quotientFloat := x / y
    fmt.Printf("x / y = %f\n", quotientFloat)
}

十一、思考

下面例子中 f1 為何會與 f2 相等?

package main

import "fmt"

func main() {
	var f1 float32 = 16777216.0
	var f2 float32 = 16777217.0
	fmt.Println(f1 == f2) // true
}

歡迎在留言區留下你的答案。

相關文章