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 時表示的值是不同的:
在同樣的位元位表示下,當最高位元位被解釋為符號位時,它代表一個有符號整型(int8),它表示的值為 -127;當最高位元位不被解釋為符號位時,它代表一個無符號整型 (uint8),它表示的值為 129。
即便最高位元位被解釋為符號位,上面的有符號整型所表示值也應該為 -1 啊,怎麼會是 -127 呢?
這是因為 Go 採用 2 的補碼(Two’s Complement)作為整型的位元位編碼方法。因此,我們不能簡單地將最高位元位看成負號,把其餘位元位表示的值看成負號後面的數值。Go 的補碼是透過原碼逐位取反後再加 1 得到的,比如,我們以 -127 這個值為例,它的補碼轉換過程就是這樣的:
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語言支援兩種浮點型數:float32
和float64
。這兩種浮點型資料格式遵循IEEE 754
標準。
浮點型別與前面的整型相比,Go 提供的浮點型別都是平臺無關的。
2.3 float32
與float64
這兩種浮點型別有什麼異同點呢?
無論是 float32
還是 float64
,它們的變數的預設值都為 0.0,不同的是它們佔用的記憶體空間大小是不一樣的,可以表示的浮點數的範圍與精度也不同。那麼浮點數在記憶體中的二進位制表示究竟是怎麼樣的呢?
浮點數在記憶體中的二進位制表示(Bit Representation)要比整型複雜得多,IEEE 754 規範給出了在記憶體中儲存和表示一個浮點數的標準形式,見下圖:
我們看到浮點數在記憶體中的二進位制表示分三個部分:符號位、階碼(即經過換算的指數),以及尾數。這樣表示的一個浮點數,它的值等於:
其中浮點值的符號由符號位決定:當符號位為 1 時,浮點值為負值;當符號位為 0 時,浮點值為正值。公式中 offset 被稱為階碼偏移值,這個我們待會再講。
我們首先來看單精度(float32)與雙精度(float64)浮點數在階碼和尾數上的不同。這兩種浮點數的階碼與尾數所使用的位數是不一樣的,你可以看下 IEEE 754 標準中單精度和雙精度浮點數的各個部分的長度規定:
我們看到,單精度浮點型別(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。
這樣,最終浮點數 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
包中。這些常量用於表示浮點數和整數型別的最大值和最小值,以及其他數學常量。
int
和uint
族都有最大值和最小值。float32
和float64
只有最大值和最小正數, 沒有最小值。
以下是一些常見的math
包中的數字型別極值常量:
- 整數型別的極值:
math.MaxInt8
:int8
型別的最大值。math.MinInt8
:int8
型別的最小值。math.MaxInt16
:int16
型別的最大值。math.MinInt16
:int16
型別的最小值。math.MaxInt32
:int32
型別的最大值。math.MinInt32
:int32
型別的最小值。math.MaxInt64
:int64
型別的最大值。math.MinInt64
:int64
型別的最小值。
- 浮點數型別的極值:
math.MaxFloat32
:float32
型別的最大正有限值。math.SmallestNonzeroFloat32
:float32
型別的最小正非零值。math.MaxFloat64
:float64
型別的最大正有限值。math.SmallestNonzeroFloat64
:float64
型別的最小正非零值。
- 其他數學常量:
math.Pi
: 圓周率 π 的近似值。math.E
: 自然對數的底數 e 的近似值。math.Sqrt2
和math.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
家族:int8
、int16
、int32
、int64
、int
。在記憶體中分別用 1、2、4、8 個位元組來表達,而 int 用幾個位元組則取決於CPU。目前大多數 int 都是 8 位元組。 如果猶豫不決,就優先使用int
,特殊情況除外。 -
uint
家族:uint8
、uint16
、uint32
、uint64
、uint
。無符號整數,類似於 int 家族,優先使用 uint。 -
float
家族:float32、float64。浮點數,優先使用float64
。
四、複數
4.1 複數
-
數學課本上將形如
z=a+bi
(a、b 均為實數,a 稱為實部,b 稱為虛部)的數稱為複數。 -
Go 語言則原生支援複數型別。
-
和整型、浮點型相比,複數型別在 Go 中的應用就更為侷限和小眾,主要用於專業領域的計算,比如向量計算等。我們簡單瞭解一下就可以了。
4.2 複數型別
-
Go 提供兩種複數型別,它們分別是
complex64
和complex128
。 -
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 注意:
-
預設值:布林型別變數的預設值為
false
。var a bool fmt.Println(a) // 輸出為false
-
強制型別轉換:Go語言不允許將整數或其他資料型別強制轉換為布林型。只有布林型別的值可以被用於布林表示式中,比如條件語句。
var x string = "2222" // 錯誤,cannot convert x (variable of type string) to type bool var isTrue bool = bool(x) fmt.Println(isTrue)
-
數值運算和轉換:布林型無法直接參與數值運算,也無法與其他資料型別進行數值轉換。這是因為布林型別只有兩個可能的值,不適合數學運算或數值轉換。
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
rune
是int32
的別名,它用於表示32位的Unicode字元,範圍從0到65535,。- rune型別代表一個UTF-8字元。實際上它是一個int32的別名。
rune
型別通常用於處理文字和字串,特別是當需要支援多語言字符集(如UTF-8編碼的Unicode字元)時。比如:需要處理中文、日文或者其他複合字元時,則需要用到rune
型別。
例如:
var a rune = '國'
7.4 簡單示例
以下是一些示例,演示了byte
和 rune
型別的用法:
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)
}
}
在上述示例中,我們首先使用 byte
和 rune
分別表示單個ASCII字元和Unicode字元。然後,我們使用 rune
處理包含多個Unicode字元的字串。
總之,byte
和 rune
型別在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
}
歡迎在留言區留下你的答案。