GO語言————4.5 基本型別和運算子

FLy_鵬程萬里發表於2018-06-26

4.5 基本型別和運算子

我們將在這個部分講解有關布林型、數字型和字元型的相關知識。

表示式是一種特定的型別的值,它可以由其它的值以及運算子組合而成。每個型別都定義了可以和自己結合的運算子集合,如果你使用了不在這個集合中的運算子,則會在編譯時獲得編譯錯誤。

一元運算子只可以用於一個值的操作(作為字尾),而二元運算子則可以和兩個值或者運算元結合(作為中綴)。

只有兩個型別相同的值才可以和二元運算子結合,另外要注意的是,Go 是強型別語言,因此不會進行隱式轉換,任何不同型別之間的轉換都必須顯式說明(第 4.2 節)。Go 不存在像 C 和 Java 那樣的運算子過載,表示式的解析順序是從左至右。

你可以在第 4.5.3 節找到有關運算子優先順序的相關資訊,優先順序越高的運算子在條件相同的情況下將被優先執行。但是你可以通過使用括號將其中的表示式括起來,以人為地提升某個表示式的運算優先順序。

4.5.1 布林型別 bool

一個簡單的例子:var b bool = true

布林型的值只可以是常量 true 或者 false。

兩個型別相同的值可以使用相等 == 或者不等 != 運算子來進行比較並獲得一個布林型的值。

當相等運算子兩邊的值是完全相同的值的時候會返回 true,否則返回 false,並且只有在兩個的值的型別相同的情況下才可以使用。

示例:

var aVar = 10
aVar == 5 -> false
aVar == 10 -> true

當不等運算子兩邊的值是不同的時候會返回 true,否則返回 false。

示例:

var aVar = 10
aVar != 5 -> true
aVar != 10 -> false

Go 對於值之間的比較有非常嚴格的限制,只有兩個型別相同的值才可以進行比較,如果值的型別是介面(interface,第 11 章),它們也必須都實現了相同的介面。如果其中一個值是常量,那麼另外一個值的型別必須和該常量型別相相容的。如果以上條件都不滿足,則其中一個值的型別必須在被轉換為和另外一個值的型別相同之後才可以進行比較。

布林型的常量和變數也可以通過和邏輯運算子(非 !、和 &&、或 ||)結合來產生另外一個布林值,這樣的邏輯語句就其本身而言,並不是一個完整的 Go 語句。

邏輯值可以被用於條件結構中的條件語句(第 5 章),以便測試某個條件是否滿足。另外,和 &&、或 || 與相等 == 或不等!= 屬於二元運算子,而非 ! 屬於一元運算子。在接下來的內容中,我們會使用 T 來代表條件符合的語句,用 F 來代表條件不符合的語句。

Go 語言中包含以下邏輯運算子:

非運算子:!

!T -> false
!F -> true

非運算子用於取得和布林值相反的結果。

和運算子:&&

T && T -> true
T && F -> false
F && T -> false
F && F -> false

只有當兩邊的值都為 true 的時候,和運算子的結果才是 true。

或運算子:||

T || T -> true
T || F -> true
F || T -> true
F || F -> false

只有當兩邊的值都為 false 的時候,或運算子的結果才是 false,其中任意一邊的值為 true 就能夠使得該表示式的結果為 true。

在 Go 語言中,&& 和 || 是具有快捷性質的運算子,當運算子左邊表示式的值已經能夠決定整個表示式的值的時候(&& 左邊的值為 false,|| 左邊的值為 true),運算子右邊的表示式將不會被執行。利用這個性質,如果你有多個條件判斷,應當將計算過程較為複雜的表示式放在運算子的右側以減少不必要的運算。

利用括號同樣可以升級某個表示式的運算優先順序。

在格式化輸出時,你可以使用 %t 來表示你要輸出的值為布林型。

布林值(以及任何結果為布林值的表示式)最常用在條件結構的條件語句中,例如:if、for 和 switch 結構(第 5 章)。

對於布林值的好的命名能夠很好地提升程式碼的可讀性,例如以 is 或者 Is 開頭的 isSortedisFinishedisVisible,使用這樣的命名能夠在閱讀程式碼的獲得閱讀正常語句一樣的良好體驗,例如標準庫中的 unicode.IsDigit(ch)(第 4.5.5 節)。

4.5.2 數字型別

4.5.2.1 整型 int 和浮點型 float

Go 語言支援整型和浮點型數字,並且原生支援複數,其中位的運算採用補碼(詳情參見 二的補碼 頁面)。

Go 也有基於架構的型別,例如:int、uint 和 uintptr。

這些型別的長度都是根據執行程式所在的作業系統型別所決定的:

  • int 和 uint 在 32 位作業系統上,它們均使用 32 位(4 個位元組),在 64 位作業系統上,它們均使用 64 位(8 個位元組)。
  • uintptr 的長度被設定為足夠存放一個指標即可。

Go 語言中沒有 float 型別。

與作業系統架構無關的型別都有固定的大小,並在型別的名稱中就可以看出來:

整數:

  • int8(-128 -> 127)
  • int16(-32768 -> 32767)
  • int32(-2,147,483,648 -> 2,147,483,647)
  • int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)

無符號整數:

  • uint8(0 -> 255)
  • uint16(0 -> 65,535)
  • uint32(0 -> 4,294,967,295)
  • uint64(0 -> 18,446,744,073,709,551,615)

浮點型(IEEE-754 標準):

  • float32(+- 1e-45 -> +- 3.4 * 1e38)
  • float64(+- 5 * 1e-324 -> 107 * 1e308)

int 型是計算最快的一種型別。

整型的零值為 0,浮點型的零值為 0.0。

float32 精確到小數點後 7 位,float64 精確到小數點後 15 位。由於精確度的緣故,你在使用 == 或者 != 來比較浮點數時應當非常小心。你最好在正式使用前測試對於精確度要求較高的運算。

你應該儘可能地使用 float64,因為 math 包中所有有關數學運算的函式都會要求接收這個型別。

你可以通過增加字首 0 來表示 8 進位制數(如:077),增加字首 0x 來表示 16 進位制數(如:0xFF),以及使用 e 來表示 10 的連乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23。

你可以使用 a := uint64(0) 來同時完成型別轉換和賦值操作,這樣 a 的型別就是 uint64。

Go 中不允許不同型別之間的混合使用,但是對於常量的型別限制非常少,因此允許常量之間的混合使用,下面這個程式很好地解釋了這個現象(該程式無法通過編譯):

示例 4.8 type_mixing.go

package main

func main() {
	var a int
	var b int32
	a = 15
	b = a + a	 // 編譯錯誤
	b = b + 5    // 因為 5 是常量,所以可以通過編譯
}

如果你嘗試編譯該程式,則將得到編譯錯誤 cannot use a + a (type int) as type int32 in assignment

同樣地,int16 也不能夠被隱式轉換為 int32。

下面這個程式展示了通過顯式轉換來避免這個問題(第 4.2 節)。

示例 4.9 casting.go

package main

import "fmt"

func main() {
	var n int16 = 34
	var m int32
	// compiler error: cannot use n (type int16) as type int32 in assignment
	//m = n
	m = int32(n)

	fmt.Printf("32 bit int is: %d\n", m)
	fmt.Printf("16 bit int is: %d\n", n)
}

輸出:

32 bit int is: 34
16 bit int is: 34

格式化說明符

在格式化字串裡,%d 用於格式化整數(%x 和 %X 用於格式化 16 進製表示的數字),%g 用於格式化浮點型(%f 輸出浮點數,%e 輸出科學計數表示法),%0d 用於規定輸出定長的整數,其中開頭的數字 0 是必須的。

%n.mg 用於表示數字 n 並精確到小數點後 m 位,除了使用 g 之外,還可以使用 e 或者 f,例如:使用格式化字串 %5.2e 來輸出 3.4 的結果為 3.40e+00

數字值轉換

當進行類似 a32bitInt = int32(a32Float) 的轉換時,小數點後的數字將被丟棄。這種情況一般發生當從取值範圍較大的型別轉換為取值範圍較小的型別時,或者你可以寫一個專門用於處理型別轉換的函式來確保沒有發生精度的丟失。下面這個例子展示如何安全地從 int 型轉換為 int8:

func Uint8FromInt(n int) (uint8, error) {
	if 0 <= n && n <= math.MaxUint8 { // conversion is safe
		return uint8(n), nil
	}
	return 0, fmt.Errorf("%d is out of the uint8 range", n)
}

或者安全地從 float64 轉換為 int:

func IntFromFloat64(x float64) int {
	if math.MinInt32 <= x && x <= math.MaxInt32 { // x lies in the integer range
		whole, fraction := math.Modf(x)
		if fraction >= 0.5 {
			whole++
		}
		return int(whole)
	}
	panic(fmt.Sprintf("%g is out of the int32 range", x))
}

不過如果你實際存的數字超出你要轉換到的型別的取值範圍的話,則會引發 panic(第 13.2 節)。

問題 4.1 int 和 int64 是相同的型別嗎?

4.5.2.2 複數

Go 擁有以下複數型別:

complex64 (32 位實數和虛數)
complex128 (64 位實數和虛數)

複數使用 re+imI 來表示,其中 re 代表實數部分,im 代表虛數部分,I 代表根號負 1。

示例:

var c1 complex64 = 5 + 10i
fmt.Printf("The value is: %v", c1)
// 輸出: 5 + 10i

如果 re 和 im 的型別均為 float32,那麼型別為 complex64 的複數 c 可以通過以下方式來獲得:

c = complex(re, im)

函式 real(c) 和 imag(c) 可以分別獲得相應的實數和虛數部分。

在使用格式化說明符時,可以使用 %v 來表示複數,但當你希望只表示其中的一個部分的時候需要使用 %f

複數支援和其它數字型別一樣的運算。當你使用等號 == 或者不等號 != 對複數進行比較運算時,注意對精確度的把握。cmath 包中包含了一些操作複數的公共方法。如果你對記憶體的要求不是特別高,最好使用 complex128 作為計算型別,因為相關函式都使用這個型別的引數。

4.5.2.3 位運算

位運算只能用於整數型別的變數,且需當它們擁有等長位模式時。

%b 是用於表示位的格式化識別符號。

二元運算子

  • 按位與 &

    對應位置上的值經過和運算結果,具體參見和運算子,第 4.5.1 節,並將 T(true)替換為 1,將 F(false)替換為 0

      1 & 1 -> 1
      1 & 0 -> 0
      0 & 1 -> 0
      0 & 0 -> 0
  • 按位或 |

    對應位置上的值經過或運算結果,具體參見或運算子,第 4.5.1 節,並將 T(true)替換為 1,將 F(false)替換為 0

      1 | 1 -> 1
      1 | 0 -> 1
      0 | 1 -> 1
      0 | 0 -> 0
  • 按位異或 ^

    對應位置上的值根據以下規則組合:

      1 ^ 1 -> 0
      1 ^ 0 -> 1
      0 ^ 1 -> 1
      0 ^ 0 -> 0
  • 位清除 &^:將指定位置上的值設定為 0。

一元運算子

  • 按位補足 ^

    該運算子與異或運算子一同使用,即 m^x,對於無符號 x 使用“全部位設定為 1”,對於有符號 x 時使用 m=-1。例如:

      ^2 = ^10 = -01 ^ 10 = -11
  • 位左移 <<

    • 用法:bitP << n

    • bitP 的位向左移動 n 位,右側空白部分使用 0 填充;如果 n 等於 2,則結果是 2 的相應倍數,即 2 的 n 次方。例如:

        1 << 10 // 等於 1 KB
        1 << 20 // 等於 1 MB
        1 << 30 // 等於 1 GB
  • 位右移 >>

    • 用法:bitP >> n
    • bitP 的位向右移動 n 位,左側空白部分使用 0 填充;如果 n 等於 2,則結果是當前值除以 2 的 n 次方。

當希望把結果賦值給第一個運算元時,可以簡寫為 a <<= 2 或者 b ^= a & 0xffffffff

位左移常見實現儲存單位的用例

使用位左移與 iota 計數配合可優雅地實現儲存單位的常量列舉:

type ByteSize float64
const (
	_ = iota // 通過賦值給空白識別符號來忽略值
	KB ByteSize = 1<<(10*iota)
	MB
	GB
	TB
	PB
	EB
	ZB
	YB
)

在通訊中使用位左移表示標識的用例

type BitFlag int
const (
	Active BitFlag = 1 << iota // 1 << 0 == 1
	Send // 1 << 1 == 2
	Receive // 1 << 2 == 4
)

flag := Active | Send // == 3

4.5.2.4 邏輯運算子

Go 中擁有以下邏輯運算子:==!=(第 4.5.1 節)、<<=>>=

它們之所以被稱為邏輯運算子是因為它們的運算結果總是為布林值 bool。例如:

b3:= 10 > 5 // b3 is true

4.5.2.5 算術運算子

常見可用於整數和浮點數的二元運算子有 +-* 和 /

(相對於一般規則而言,Go 在進行字串拼接時允許使用對運算子 + 的過載,但 Go 本身不允許開發者進行自定義的運算子過載)

/ 對於整數運算而言,結果依舊為整數,例如:9 / 4 -> 2

取餘運算子只能作用於整數:9 % 4 -> 1

整數除以 0 可能導致程式崩潰,將會導致執行時的恐慌狀態(如果除以 0 的行為在編譯時就能被捕捉到,則會引發編譯錯誤);第 13 章將會詳細講解如何正確地處理此類情況。

浮點數除以 0.0 會返回一個無窮盡的結果,使用 +Inf 表示。

練習 4.4 嘗試編譯 divby0.go

你可以將語句 b = b + a 簡寫為 b+=a,同樣的寫法也可用於 -=*=/=%=

對於整數和浮點數,你可以使用一元運算子 ++(遞增)和 --(遞減),但只能用於字尾:

i++ -> i += 1 -> i = i + 1
i-- -> i -= 1 -> i = i - 1

同時,帶有 ++ 和 -- 的只能作為語句,而非表示式,因此 n = i++ 這種寫法是無效的,其它像 f(i++) 或者a[i]=b[i++] 這些可以用於 C、C++ 和 Java 中的寫法在 Go 中也是不允許的。

在運算時 溢位 不會產生錯誤,Go 會簡單地將超出位數拋棄。如果你需要範圍無限大的整數或者有理數(意味著只被限制於計算機記憶體),你可以使用標準庫中的 big 包,該包提供了類似 big.Int 和 big.Rat 這樣的型別(第 9.4 節)。

4.5.2.6 隨機數

一些像遊戲或者統計學類的應用需要用到隨機數。rand 包實現了偽隨機數的生成。

示例 4.10 random.go 演示瞭如何生成 10 個非負隨機數:

package main
import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	for i := 0; i < 10; i++ {
		a := rand.Int()
		fmt.Printf("%d / ", a)
	}
	for i := 0; i < 5; i++ {
		r := rand.Intn(8)
		fmt.Printf("%d / ", r)
	}
	fmt.Println()
	timens := int64(time.Now().Nanosecond())
	rand.Seed(timens)
	for i := 0; i < 10; i++ {
		fmt.Printf("%2.2f / ", 100*rand.Float32())
	}
}

可能的輸出:

816681689 / 1325201247 / 623951027 / 478285186 / 1654146165 /
1951252986 / 2029250107 / 762911244 / 1372544545 / 591415086 / / 3 / 0 / 6 / 4 / 2 /22.10
/ 65.77 / 65.89 / 16.85 / 75.56 / 46.90 / 55.24 / 55.95 / 25.58 / 70.61 /

函式 rand.Float32 和 rand.Float64 返回介於 [0.0, 1.0) 之間的偽隨機數,其中包括 0.0 但不包括 1.0。函式 rand.Intn 返回介於 [0, n) 之間的偽隨機數。

你可以使用 Seed(value) 函式來提供偽隨機數的生成種子,一般情況下都會使用當前時間的納秒級數字(第 4.8 節)。

4.5.3 運算子與優先順序

有些運算子擁有較高的優先順序,二元運算子的運算方向均是從左至右。下表列出了所有運算子以及它們的優先順序,由上至下代表優先順序由高到低:

優先順序 	運算子
 7 		^ !
 6 		* / % << >> & &^
 5 		+ - | ^
 4 		== != < <= >= >
 3 		<-
 2 		&&
 1 		||

當然,你可以通過使用括號來臨時提升某個表示式的整體運算優先順序。

4.5.4 型別別名

當你在使用某個型別時,你可以給它起另一個名字,然後你就可以在你的程式碼中使用新的名字(用於簡化名稱或解決名稱衝突)。

在 type TZ int 中,TZ 就是 int 型別的新名稱(用於表示程式中的時區),然後就可以使用 TZ 來操作 int 型別的資料。

示例 4.11 type.go

package main
import "fmt"

type TZ int

func main() {
	var a, b TZ = 3, 4
	c := a + b
	fmt.Printf("c has the value: %d", c) // 輸出:c has the value: 7
}

實際上,型別別名得到的新型別並非和原型別完全相同,新型別不會擁有原型別所附帶的方法(第 10 章);TZ 可以自定義一個方法用來輸出更加人性化的時區資訊。

練習 4.5 定義一個 string 的型別別名 Rope,並宣告一個該型別的變數。

4.5.5 字元型別

嚴格來說,這並不是 Go 語言的一個型別,字元只是整數的特殊用例。byte 型別是 uint8 的別名,對於只佔用 1 個位元組的傳統 ASCII 編碼的字元來說,完全沒有問題。例如:var ch byte = 'A';字元使用單引號括起來。

在 ASCII 碼錶中,A 的值是 65,而使用 16 進製表示則為 41,所以下面的寫法是等效的:

var ch byte = 65var ch byte = '\x41'

\x 總是緊跟著長度為 2 的 16 進位制數)

另外一種可能的寫法是 \ 後面緊跟著長度為 3 的八進位制數,例如:\377

不過 Go 同樣支援 Unicode(UTF-8),因此字元同樣稱為 Unicode 程式碼點或者 runes,並在記憶體中使用 int 來表示。在文件中,一般使用格式 U+hhhh 來表示,其中 h 表示一個 16 進位制數。其實 rune 也是 Go 當中的一個型別,並且是 int32 的別名。

在書寫 Unicode 字元時,需要在 16 進位制數之前加上字首 \u 或者 \U

因為 Unicode 至少佔用 2 個位元組,所以我們使用 int16 或者 int 型別來表示。如果需要使用到 4 位元組,則會加上 \U 字首;字首 \u 則總是緊跟著長度為 4 的 16 進位制數,字首 \U 緊跟著長度為 8 的 16 進位制數。

示例 4.12 char.go

var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3) // UTF-8 code point

輸出:

65 - 946 - 1053236
A - β - r
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234

格式化說明符 %c 用於表示字元;當和字元配合使用時,%v 或 %d 會輸出用於表示該字元的整數;%U 輸出格式為 U+hhhh 的字串(另一個示例見第 5.4.4 節)。

包 unicode 包含了一些針對測試字元的非常有用的函式(其中 ch 代表字元):

  • 判斷是否為字母:unicode.IsLetter(ch)
  • 判斷是否為數字:unicode.IsDigit(ch)
  • 判斷是否為空白符號:unicode.IsSpace(ch)

這些函式返回一個布林值。包 utf8 擁有更多與 rune 相關的函式。

( 譯者注:關於型別的相關講解,可參考視訊教程 《Go程式設計基礎》 第 3 課:型別與變數 )

相關文章