Go基礎編程:數據類型

 原文鏈接

http://oldchen.iwulai.com/index.php/2019/01/10/go%E5%9F%BA%E7%A1%80%E7%BC%96%E7%A8%8B%EF%BC%9A%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/

Go語言內置以下這些基礎類型: 

  1.  布爾類型:bool。
  2.  整型:int8、byte、int16、int、uint、uintptr等。
  3.  浮點類型:float32、float64。
  4.  複數類型:complex64、complex128。
  5.  字符串:string。
  6.  字符類型:rune。
  7.  錯誤類型:error。

此外,Go語言也支持以下這些複合類型:

  •  指針(pointer)
  •  數組(array)
  •  切片(slice)
  •  字典(map)
  •  通道(chan)
  •  結構體(struct)
  •  接口(interface)

1.布爾類型

Go語言中的布爾類型與其他語言基本一致,關鍵字也爲bool,可賦值爲預定義的true和false示例代碼如下:

var v1 bool
v1 = true
v2 := (1 == 2) // v2也會被推導爲bool類型

布爾類型不能接受其他類型的賦值,不支持自動或強制的類型轉換。以下的示例是一些錯誤的用法,會導致編譯錯誤:

var b bool
b = 1 // 編譯錯誤
b = bool(1) // 編譯錯誤

以下的用法纔是正確的:

var b bool
b = (1!=0) // 編譯正確 
fmt.Println("Result:", b) // 打印結果爲Result: true 

2.整型

整型是所有編程語言裏最基礎的數據類型。Go語言支持表2-1所示的這些整型類型。

2.1. 類型表示

需要注意的是,int和int32在Go語言裏被認爲是兩種不同的類型,編譯器也不會幫你自動做類型轉換,比如以下的例子會有編譯錯誤:

var value2 int32
value1 := 64    // value1將會被自動推導爲int類型
value2 = value1 // 編譯錯誤

編譯錯誤類似於:

cannot use value1 (type int) as type int32 in assignment。

使用強制類型轉換可以解決這個編譯錯誤:

value2 = int32(value1) // 編譯通過

當然,開發者在做強制類型轉換時,需要注意數據長度被截短而發生的數據精度損失(比如將浮點數強制轉爲整數)和值溢出(值超過轉換的目標類型的值範圍時)問題。

2.2. 數值運算

Go語言支持下面的常規整數運算:+、-、*、/和%。加減乘除就不詳細解釋了,需要說下的是,% 和在C語言中一樣是求餘運算,比如:

5 % 3 // 結果爲:2 

2.3. 比較運算

Go語言支持以下的幾種比較運算符:>、<、==、>=、<=和!=。這一點與大多數其他語言相同,與C語言完全一致。
下面爲條件判斷語句的例子:

i, j := 1, 2 
if i == j { 
  fmt.Println("i and j are equal.") 
} 

兩個不同類型的整型數不能直接比較,比如int8類型的數和int類型的數不能直接比較,但各種類型的整型變量都可以直接與字面常量(literal)進行比較,比如:

package main
import "fmt"
func main() {
var i int32
var j int64
i, j = 1, 2 

if i == 1 || j == 2 { // 編譯通過
 fmt.Println("i and j are equal.") 
} 
if i == j { // 編譯錯誤
 fmt.Println("i and j are equal.") 
} 
}

3.浮點型

浮點型用於表示包含小數點的數據,比如1.234就是一個浮點型數據。Go語言中的浮點類型採用IEEE-754標準的表達方式。

3.1. 浮點數表示

Go語言定義了兩個類型float32和float64,其中float32等價於C語言的float類型,
float64等價於C語言的double類型。
在Go語言裏,定義一個浮點數變量的代碼如下:

var fvalue1 float32
fvalue1 = 12 
fvalue2 := 12.0 // 如果不加小數點,fvalue2會被推導爲整型而不是浮點型

對於以上例子中類型被自動推導的fvalue2,需要注意的是其類型將被自動設爲float64,而不管賦給它的數字是否是用32位長度表示的。因此,對於以上的例子,下面的賦值將導致編譯錯誤:

fvalue1 = fvalue2 
而必須使用這樣的強制類型轉換:
fvalue1 = float32(fvalue2) 

3.2. 浮點數比較

因爲浮點數不是一種精確的表達方式,所以像整型那樣直接用==來判斷兩個浮點數是否相等
是不可行的,這可能會導致不穩定的結果。
下面是一種推薦的替代方案:

import "math" 
// p爲用戶自定義的比較精度,比如0.00001 
func IsEqual(f1, f2, p float64) bool { 
 return math.Fdim(f1, f2) < p 
} 

4.複數類型


複數實際上由兩個實數(在計算機中用浮點數表示)構成,一個表示實部(real),一個表示虛部(imag)。和數學上的複數是一回事。

4.1. 複數表示

複數表示的示例如下:

var value1 complex64 // 由2個float32構成的複數類型
value1 = 3.2 + 12i 
value2 := 3.2 + 12i // value2是complex128類型
value3 := complex(3.2, 12) // value3結果同 value2 

4.2. 實部與虛部

對於一個複數z = complex(x, y),就可以通過Go語言內置函數real(z)獲得該複數的實部,也就是x,通過imag(z)獲得該複數的虛部,也就是y。
更多關於複數的函數,請查閱math/cmplx標準庫的文檔。

5.字符串

在Go語言中,字符串也是一種基本類型。相比之下, C/C++語言中並不存在原生的字符串類型,通常使用字符數組來表示,並以字符指針來傳遞。
Go語言中字符串的聲明和初始化非常簡單,舉例如下:

package main
import "fmt"
func main() {
    var a string // 聲明一個字符串變量
    a = "Hello world" // 字符串賦值
    ch := a[0]
    fmt.Printf("字符串爲:",a,"長度爲:",len(a),"第一個字符:",ch)
}

輸出結果爲:

字符串爲:%!(EXTRA string=Hello world, string=長度爲:, int=11, string=第一個字符:, uint8=72)

字符串的內容可以用類似於數組下標的方式獲取,但與數組不同,字符串的內容不能在初始化後被修改,比如以下的例子:

str := "Hello world" // 字符串也支持聲明時進行初始化的做法
str[0] = 'X' // 編譯錯誤

編譯器會報類似如下的錯誤:

cannot assign to str[0] 

Go語言內置的函數len()來取字符串的長度。這個函數非常有用,在實際開發過程中處理字符串、數組和切片時將會經常用到。

5.1. 字符串操作

平時常用的字符串操作如表2-3所示。


5.2. 字符串遍歷

Go語言支持兩種方式遍歷字符串。

一種是以字節數組的方式遍歷:

package main
import "fmt"
func main() {
    str := "beijing,北京"
    for i := 0; i < len(str); i++{
        fmt.Println(i, " ", str[i])
    } 
}

這個例子的輸出結果爲:

可以看出,字符串長度爲13。從直觀上來說,這個字符串應該只有9個字符。這是因爲中文字符在UTF-8中佔3個字節。
一種是以Unicode字符遍歷:

package main
import "fmt"
func main() {
    str := "beijing,北京"
    for index, val := range str {
        fmt.Println(index, " ", val)
    }
}

輸出結果爲:

以Unicode字符方式遍歷時,每個字符的類型是rune,(早期的Go語言用int類型表示Unicode字符)而不是byte。rune類型在go語言中佔用四個字節。

5.3 .字符類型

在go語言中支持兩個字符類型:

一個是byte(實際上是uint8的別名),代表UTF-8字符串的單個字節的值,

一個是rune,代表單個Unicode字符。

6.數組

數組就是指一系列同一類型數據的集合。數組中包含的每個數據被稱爲數組元素(element),一個數組包含的元素個數被稱爲數組的長度。
以下爲一些常規的數組聲明方法:

[32]byte                    // 長度爲32的數組,每個元素爲一個字節
[2*N] struct { x, y int32 } // 複雜類型數組
[1000]*float64              // 指針數組
[3][5]int                   // 二維數組
[2][2][2]float64            // 等同於[2]([2]([2]float64)) 

從以上類型也可以看出,數組可以是多維的,比如[3][5]int就表達了一個3行5列的二維整型數組,總共可以存放15個整型元素。


在Go語言中,數組長度在定義後就不可更改,在聲明時長度可以爲一個常量或者一個常量表達式(常量表達式是指在編譯期即可計算結果的表達式)。數組的長度是該數組類型的一個內置常量,可以用Go語言的內置函數len()來獲取。下面是一個獲取數組arr元素個數的寫法:

arrLength := len(arr) 

6.1. 元素訪問

可以使用數組下標來訪問數組中的元素。與C語言相同,數組下標從0開始,len(array)-1則表示最後一個元素的下標。下面的示例遍歷整型數組並逐個打印元素內容:

for i := 0; i < len(array); i++ { 
 fmt.Println("Element", i, "of array is", array[i]) 
} 

Go語言還提供了一個關鍵字range,用於便捷地遍歷容器中的元素。當然,數組也是range的支持範圍。上面的遍歷過程可以簡化爲如下的寫法:

for i, v := range array { 
 fmt.Println("Array element[", i, "]=", v) 
} 

在上面的例子裏可以看到,range具有兩個返回值,第一個返回值是元素的數組下標,第二個返回值是元素的值。

6.2. 值類型

需要特別注意的是,在Go語言中數組是一個值類型(value type)。所有的值類型變量在賦值和作爲參數傳遞時都將產生一次複製動作。如果將數組作爲函數的參數類型,則在函數調用時該參數將發生數據複製。因此,在函數體中無法修改傳入的數組的內容,因爲函數內操作的只是所傳入數組的一個副本。下面用例子來說明這一特點:

package main 
import "fmt" 
func modify(array [10]int) { 
 array[0] = 10 // 試圖修改數組的第一個元素
 fmt.Println("In modify(), array values:", array) 
} 
func main() { 
 array := [5]int{1,2,3,4,5} // 定義並初始化一個數組
 modify(array) // 傳遞給一個函數,並試圖在函數體內修改這個數組內容
 fmt.Println("In main(), array values:", array) 
} 

該程序的執行結果爲:

In modify(), array values: [10 2 3 4 5] 
In main(), array values: [1 2 3 4 5] 

從執行結果可以看出,函數modify()內操作的那個數組跟main()中傳入的數組是兩個不同的實例。那麼,如何才能在函數內操作外部的數據結構呢?接下來將要詳細介紹如何用數組切片功能來達成這個目標。

7.數組切片

在前一節裏我們已經提過數組的特點:數組的長度在定義之後無法再次修改;數組是值類型,每次傳遞都將產生一份副本。顯然這種數據結構無法完全滿足開發者的真實需求。不用失望,Go語言提供了數組切片(slice)這個非常酷的功能來彌補數組的不足。初看起來,數組切片就像一個指向數組的指針,實際上它擁有自己的數據結構,而不僅僅是個指針。數組切片的數據結構可以抽象爲以下3個變量:
 一個指向原生數組的指針;
 數組切片中的元素個數;
 數組切片已分配的存儲空間。

7.1. 創建數組切片

創建數組切片的方法主要有兩種——基於數組和直接創建,下面我們來簡要介紹一下這兩種方法。
 基於數組
數組切片可以基於一個已存在的數組創建。數組切片可以只使用數組的一部分元素或者整個數組來創建,甚至可以創建一個比所基於的數組還要大的數組切片。下面演示瞭如何基於一個數組的前5個元素創建一個數組切片。

package main
import "fmt"
func main() {
// 先定義一個數組
var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 基於數組創建一個數組切片
var mySlice []int = myArray[:5]
fmt.Println("Elements of myArray: ")
for _, v := range myArray {
    fmt.Print(v, " ")
}
fmt.Println("\nElements of mySlice: ")
for _, v := range mySlice {
   fmt.Print(v, " ")
}
  fmt.Println()
}

運行結果爲:

Elements of myArray: 
1 2 3 4 5 6 7 8 9 10 
Elements of mySlice: 
1 2 3 4 5 

應該已經注意到,Go語言支持用myArray[first:last]這樣的方式來基於數組生成一個數組切片,而且這個用法還很靈活,比如下面幾種都是合法的。

基於myArray的所有元素創建數組切片:
mySlice = myArray[:] 
基於myArray的前5個元素創建數組切片:
mySlice = myArray[:5]
基於從第5個元素開始的所有元素創建數組切片:
mySlice = myArray[5:] 

 直接創建
並非一定要事先準備一個數組才能創建數組切片。Go語言提供的內置函數make()可以用於靈活地創建數組切片。下面的例子示範了直接創建數組切片的各種方法。

創建一個初始元素個數爲5的數組切片,元素初始值爲0:
mySlice1 := make([]int, 5) 
創建一個初始元素個數爲5的數組切片,元素初始值爲0,並預留10個元素的存儲空間:
mySlice2 := make([]int, 5, 10) 
直接創建並初始化包含5個元素的數組切片:
mySlice3 := []int{1, 2, 3, 4, 5} 

當然,事實上還會有一個匿名數組被創建出來,只是不需要我們來操心而已。

7.2. 元素遍歷

操作數組元素的所有方法都適用於數組切片,比如數組切片也可以按下標讀寫元素,用len()
函數獲取元素個數,並支持使用range關鍵字來快速遍歷所有元素。
傳統的元素遍歷方法如下: 
for i := 0; i <len(mySlice); i++ { 
 fmt.Println("mySlice[", i, "] =", mySlice[i]) 

使用range關鍵字可以讓遍歷代碼顯得更整潔。range表達式有兩個返回值,第一個是索引,
第二個是元素的值:
for i, v := range mySlice { 
 fmt.Println("mySlice[", i, "] =", v) 

對比上面的兩個方法,我們可以很容易地看出使用range的代碼更簡單易懂。

7.3. 動態增減元素

可動態增減元素是數組切片比數組更爲強大的功能。與數組相比,數組切片多了一個存儲能力(capacity)的概念,即元素個數和分配的空間可以是兩個不同的值。合理地設置存儲能力的值,可以大幅降低數組切片內部重新分配內存和搬送內存塊的頻率,從而大大提高程序性能。
假如你明確知道當前創建的數組切片最多可能需要存儲的元素個數爲50,那麼如果你設置的存儲能力小於50,比如20,那麼在元素超過20時,底層將會發生至少一次這樣的動作——重新分配一塊“夠大”的內存,並且需要把內容從原來的內存塊複製到新分配的內存塊,這會產生比較明顯的開銷。給“夠大”這兩個字加上引號的原因是系統並不知道多大才是夠大,所以只是一個簡單的猜測。比如,將原有的內存空間擴大兩倍,但兩倍並不一定夠,所以之前提到的內存重新分配和內容複製的過程很有可能發生多次,從而明顯降低系統的整體性能。但如果你知道最大是50並且一開始就設置存儲能力爲50,那麼之後就不會發生這樣非常耗費CPU的動作,從而達到空間換時間的效果。
數組切片支持Go語言內置的cap()函數和len()函數,代碼清單2-2簡單示範了這兩個內置函數的用法。可以看出,cap()函數返回的是數組切片分配的空間大小,而len()函數返回的是數組切片中當前所存儲的元素個數。
代碼清單2-2 slice2.go 

package main
import "fmt"
func main() {
mySlice := make([]int, 5, 10)
    fmt.Println("len(mySlice):", len(mySlice))
    fmt.Println("cap(mySlice):", cap(mySlice))
}

該程序的輸出結果爲:

len(mySlice): 5 
cap(mySlice): 10 

如果需要往上例中mySlice已包含的5個元素後面繼續新增元素,可以使用append()函數。
下面的代碼可以從尾端給mySlice加上3個元素,從而生成一個新的數組切片:

mySlice = append(mySlice, 1, 2, 3) 

函數append()的第二個參數其實是一個不定參數,我們可以按自己需求添加若干個元素,甚至將一個數組切片追加到另一個數組切片的末尾:

mySlice2 := []int{8, 9, 10} 
// 給mySlice後面添加另一個數組切片
mySlice = append(mySlice, mySlice2...) 

需要注意的是,我們在第二個參數mySlice2後面加了三個點,即一個省略號,如果沒有這個省略號的話,會有編譯錯誤,因爲按append()的語義,從第二個參數起的所有參數都是待附加的元素。因爲mySlice中的元素類型爲int,所以直接傳遞mySlice2是行不通的。加上省略號相當於把mySlice2包含的所有元素打散後傳入。
上述調用等同於:

mySlice = append(mySlice, 8, 9, 10) 

數組切片會自動處理存儲空間不足的問題。如果追加的內容長度超過當前已分配的存儲空間(即cap()調用返回的信息),數組切片會自動分配一塊足夠大的內存。

7.4. 基於數組切片創建數組切片

類似於數組切片可以基於一個數組創建,數組切片也可以基於另一個數組切片創建。下面的例子基於一個已有數組切片創建新數組切片:

oldSlice := []int{1, 2, 3, 4, 5} 
newSlice := oldSlice[:3] // 基於oldSlice的前3個元素構建新數組切片

有意思的是,選擇的oldSlicef元素範圍甚至可以超過所包含的元素個數,比如newSlice可以基於oldSlice的前6個元素創建,雖然oldSlice只包含5個元素。只要這個選擇的範圍不超過oldSlice存儲能力(即cap()返回的值),那麼這個創建程序就是合法的。newSlice中超出oldSlice元素的部分都會填上0。

7.5. 內容複製

數組切片支持Go語言的另一個內置函數copy(),用於將內容從一個數組切片複製到另一個數組切片。如果加入的兩個數組切片不一樣大,就會按其中較小的那個數組切片的元素個數進行復制。下面的示例展示了copy()函數的行爲:

slice1 := []int{1, 2, 3, 4, 5} 
slice2 := []int{5, 4, 3} 
copy(slice2, slice1) // 只會複製slice1的前3個元素到slice2中
copy(slice1, slice2) // 只會複製slice2的3個元素到slice1的前3個位置

8.map

在C++/Java中,map一般都以庫的方式提供,比如在C++中是STL的std::map<>,在C#中是Dictionary<>,在Java中是Hashmap<>,在這些語言中,如果要使用map,事先要引用相應的庫。而在Go中,使用map不需要引入任何庫,並且用起來也更加方便。
map是一堆鍵值對的未排序集合。比如以身份證號作爲唯一鍵來標識一個人的信息,則這個map可以定義爲以下的方式。

package main 
import "fmt" 
// PersonInfo是一個包含個人詳細信息的類型
type PersonInfo struct { 
 ID string
 Name string
 Address string
} 
func main() { 
var personDB map[string] PersonInfo 
 personDB = make(map[string] PersonInfo) 
 // 往這個map裏插入幾條數據
 personDB["12345"] = PersonInfo{"12345", "Tom", "Room 203,..."} 
 personDB["1"] = PersonInfo{"1", "Jack", "Room 101,..."} 
 // 從這個map查找鍵爲"1234"的信息
 person, ok := personDB["1234"] 

// ok是一個返回的bool型,返回true表示找到了對應的數據
 if ok { 
 fmt.Println("Found person", person.Name, "with ID 1234.") 
 } else { 
 fmt.Println("Did not find person with ID 1234.") 
 } 
}

上面這個簡單的例子基本上已經覆蓋了map的主要用法,下面對其中的關鍵點進行細述。

8.1. 變量聲明

map的聲明基本上沒有多餘的元素,比如:

var myMap map[string] PersonInfo 

其中,myMap是聲明的map變量名,string是鍵的類型,PersonInfo則是其中所存放的值類型。

8.2. 創建

我們可以使用Go語言內置的函數make()來創建一個新map。

下面的這個例子創建了一個鍵類型爲string、值類型爲PersonInfo的map: 

myMap = make(map[string] PersonInfo) 

也可以選擇是否在創建時指定該map的初始存儲能力,下面的例子創建了一個初始存儲能力爲100的map: 

myMap = make(map[string] PersonInfo, 100) 

創建並初始化map的代碼如下:

myMap = map[string] PersonInfo{ 
 "1234": PersonInfo{"1", "Jack", "Room 101,..."}, 
} 

8.3. 元素賦值

賦值過程非常簡單明瞭,就是將鍵和值用下面的方式對應起來即可:

myMap["1234"] = PersonInfo{"1", "Jack", "Room 101,..."} 

8.4. 元素刪除

Go語言提供了一個內置函數delete(),用於刪除容器內的元素。下面我們簡單介紹一下如
何用delete()函數刪除map內的元素:

delete(myMap, "1234") 

上面的代碼將從myMap中刪除鍵爲“1234”的鍵值對。如果“1234”這個鍵不存在,那麼這個調
用將什麼都不發生,也不會有什麼副作用。但是如果傳入的map變量的值是nil,該調用將導致
程序拋出異常(panic)。

8.5. 元素查找

在Go語言中,map的查找功能設計得比較精巧。而在其他語言中,我們要判斷能否獲取到一個值不是件容易的事情。判斷能否從map中獲取一個值的常規做法是:

(1) 聲明並初始化一個變量爲空;
(2) 試圖從map中獲取相應鍵的值到該變量中;
(3) 判斷該變量是否依舊爲空,如果爲空則表示map中沒有包含該變量。

這種用法比較囉唆,而且判斷變量是否爲空這條語句並不能真正表意(是否成功取到對應的值),從而影響代碼的可讀性和可維護性。有些庫甚至會設計爲因爲一個鍵不存在而拋出異常,讓開發者用起來膽戰心驚,不得不一層層嵌套try-catch語句,這更是不人性化的設計。在Go語言中,要從map中查找一個特定的鍵,可以通過下面的代碼來實現:

value, ok := myMap["1234"] 
if ok { // 找到了
 // 處理找到的value 
} 

判斷是否成功找到特定的鍵,不需要檢查取到的值是否爲nil,只需查看第二個返回值ok,這讓表意清晰很多。配合:=操作符,讓你的代碼沒有多餘成分,看起來非常清晰易懂。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章