Go筆記-類型

類型

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

  • 布爾類型:bool,佔1字節
  • 整型:int八、byte(uint8)、int1六、int、uint、uintptr等。
  • 浮點類型:float3二、float64。
  • 複數類型:complex6四、complex128。
  • 字符串:string。
  • 字符類型:rune(int32)。
  • 錯誤類型:error。

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

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

golang中,nil只能賦值給指針、channel、func、interface、map或slice類型的變量.緩存

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

無符號整數:app

  • 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 型是計算最快的一種類型。函數

float32 精確到小數點後 7 位,float64 精確到小數點後 15 位。因爲精確度的緣故,你在使用 == 或者 != 來比較浮點數時應當很是當心.

你應該儘量地使用 float64,由於 math 包中全部有關數學運算的函數都會要求接收這個類型。

各種型的最大值最小值在math包中定義。

類型聲明可在builtin包中查看。

布爾類型

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

整型

【注意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】兩個不一樣類型的整型數不能直接比較,好比int8類型的數和int類型的數不能直接比較,但各類類型的整型變量均可以直接與字面常量(literal)進行比較,好比:

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

進制

fmt.Println(0x10) // 十六進制 16
fmt.Println(010)  // 八進制 8

e

2e3表示2*10^3=2000

前70個已經被緩存起來,可在math.Pow()中查看。

2e3或者2.0e3默認都是float64類型:

var i = 2e3 
reflect.TypeOf(i) == float64
var i int = 2e3
reflect.TypeOf(i) == int

int64 和 uint64 的最大位賦值

var big int64 = 1 << 63
fmt.Printf("%b\n", big)

報錯:constant 9223372036854775808 overflows int64

var big uint64 = 1 << 63
fmt.Printf("%b\n", big)

輸出: 1000000000000000000000000000000000000000000000000000000000000000

這多是由於 int64 中符號位也要佔一個 bit。


int佔多少位二進制

在 atoi.go 文件中:

const intSize = 32 << (^uint(0) >> 63)

位運算

  • x << y 左移
  • x >> y 右移
  • x ^ y 異或
  • x & y
  • x | y
  • ^x 取反

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

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


【&^】

位清除 &^:將指定位置上的值設置爲 0

n1, n2 := 7, 3
fmt.Printf("%b &^ %b = %b\n", n1, n2, n1&^n2) // 111 &^ 11 = 100 末尾1,2位置零

n1, n2 := 7, 2
fmt.Printf("%b &^ %b = %b\n", n1, n2, n1&^n2) // 111 &^ 10 = 101 末尾2位置零

【^x】

單獨使用取反,結合使用異或。

該運算符與異或運算符一同使用,即 m^x,對於無符號 x 使用「所有位設置爲 1」,對於有符號 x 時使用 m=-1.

var n uint8 = 2
fmt.Printf("^%b = %b\n", n, ^n) // ^10 = 11111101 即 253
var nn uint8 = 0xFF
fmt.Printf("%b ^ %b = %b\n", nn, n, nn^n) // 11111111 ^ 10 = 11111101

var n2 int8 = 2
fmt.Printf("^%b = %b\n", n2, ^n2) // ^10 = -11 即 -3
var nn2 int8 = -1
fmt.Printf("%b ^ %b = %b\n", nn2, n2, nn2^n2) // -1 ^ 10 = -11

對於無符號整型,取反至關於與 0xFF 異或

對於有符號整型,取反至關於與 -1 異或。

上面的操做都是以補碼的形式進行的,正數的補碼仍是自身。

取反至關於補碼和 11111111 異或,而後求補碼。


【<<】

3<<2 => 11 << 2 = 1100
-3<<1 => -11 << 1 = -6(不求補碼好像也能夠)

【>>】

var n1 uint8 = 7
var c uint8 = 2
fmt.Printf("%b >> %d = %b\n", n1, c, n1>>c) // 111 >> 2 = 1
	
var n2 int8 = 7
fmt.Printf("%b >> %d = %b\n", n2, c, n2>>c) // 111 >> 2 = 1
	
var n3 int8 = -7
fmt.Printf("%b >> %d = %b\n", n3, c, n3>>c) // -111 >> 2 = -2

對於正數,右移時左邊補0.

對於負數,以-7(-0111)爲例,求補碼(1 1001,第一位爲1表示負數,其他位取反+1,或者從右開始遇到第一個1,這個1以前的取反),將補碼右移2位,左邊補1,變成1 1110,再求補碼變成1 0010,即-2。

另外須要注意的是:-1 >> 1 == -1, 避免陷入死循環。


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

使用位左移與 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

標誌位操做

a := 0
a |= 1 << 2       // 0000100: 在bit2設置標誌位
a |= 1 << 6       // 1000100: 在bit6設置標誌位
a = a &^ (1 << 6) // 0000100: 清除bit6標誌位

移位操做右邊的數不能是有符號數

bit := 2
a := 3 << bit
fmt.Printf("%b\n", a)

報錯:invalid operation: 3 << bit (shift count type int, must be unsigned integer)

這樣使用:

var bit uint = 2
a := 3 << bit
fmt.Printf("%b\n", a) // 1100

浮點型

Go語言定義了兩個類型float32和float64,其中float32等價於C語言的float類型,float64等價於C語言的double類型

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

注意!fvalue2默認被推導爲float64

var val float32
val = 3.0
val2 := 12.4 // val2被自動推導爲float64
val = val2 // 錯誤,不能將float64賦給float32
val = float32(val2) // 使用強制轉換

浮點數比較

浮點數不是一種精確的表達方式,因此像整型那樣直接用==來判斷兩個浮點數是否相等是不可行的,這可能會致使不穩定的結果。

下面是一種推薦的替代方案:

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

一般應該優先使用float64類型,由於float32類型的累計計算偏差很容易擴散,而且float32能精確表示的正整數並非很大(譯註:由於float32的有效bit位只有23個,其它的bit位用於指數和符號;當整數大於23bit能表達的範圍時,float32的表示將出現偏差)

var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1)    // "true"!

小數點前面或後面的數字均可能被省略(例如.707或1.)。 很小或很大的數最好用科學計數法書寫,經過e或E來指定指數部分:

const Avogadro = 6.02214129e23  // 阿伏伽德羅常數
const Planck   = 6.62606957e-34 // 普朗克常數

四捨五入

for _, n := range []float64{1.4, 1.5, 1.6, 2.0} {
		fmt.Println(n, "=>", int(n+0.5))
	}
	// 1
	// 2
	// 2
	// 2

複數類型

複數類型有complex64和complex128

複數表示

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

實部和虛部

real(value1), imag(value1)

運算

複數的一些運算可在math/cmplx包中找到。

使用==或!=進行比較時注意精度。

字符串

  • Go 中的字符串根據須要佔用 1 至 4 個字節
  • 和 C/C++ 同樣,Go 中的字符串是根據長度限定,而非特殊字符。
  • 獲取字符串中某個字節的地址的行爲是非法的,例如:&str[i]。
  • str[i]的方式只對純 ASCII 碼的字符串有效。
  • 和字符串有關的包:strings, strconv, unicode
var str string      // 聲明一個字符串變量
	str = "Hello world" // 字符串賦值
	ch := str[0]        // 取字符串的第一個字符
	fmt.Printf("The length of \"%s\" is %d \n", str, len(str))
	fmt.Printf("The first character of \"%s\" is %c.\n", str, ch)

輸出結果爲:

The length of "Hello world" is 11 
The first character of "Hello world" is H.

【字符串嵌套】

var s = "ni 'hao'. "
var s = `ni "hao" .`

【字符串修改】

字符串初始化後不能被改變

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

能夠先將其轉換成[]rune或[]byte,完成後再轉換成string,不管哪一種方式,都會從新分配內存,並複製字節數組。

s := "abc"
bs := []byte(s)
bs[1] = 'B'

println(s, string(bs))

u := "電腦"
us := []rune(u)
us[1] = '話'

println(u, string(us))

輸出:

abc aBc
電腦 電話

【字符串鏈接】

s := "hello" + " world"
s := "hello" + " world " + 3 // 錯誤
s := "hello" + " world " + string(31) // 正確,但不會輸出31

+的並非最高效的作法,使用strings.Join()和bytes.Buffer更好些。

【字符串遍歷】

方式一:

str := "hello,世界"
	for i, n := 0, len(str); i < n; i++ {
		var ch2 uint8 = str[i] // 類型是byte,即uint8
		fmt.Printf("%d = '%c' , ", i, ch2)
	}

輸出:

0 = 'h' , 1 = 'e' , 2 = 'l' , 3 = 'l' , 4 = 'o' , 5 = ',' , 6 = 'ä' , 7 = '¸' , 8 = '–' , 9 = 'ç' , 10 = '•' , 11 = 'Œ' ,

長度爲12,每一箇中文字符在UTF-8中佔3個字節。

方式二:

str := "hello,世界"
	for i, ch := range str {
		fmt.Print(i, "= ")
		fmt.Print(ch, " ,") // ch的類型是rune
		fmt.Printf("'%c' .", ch)
	}

輸出:

0= 104 ,'h' .1= 101 ,'e' .2= 108 ,'l' .3= 108 ,'l' .4= 111 ,'o' .5= 44 ,',' .6= 19990 ,'世' .9= 30028 ,'界' .

【中文字符截取】

strEn := "abcdef"
	strCn := "中文測試"
	fmt.Println(strEn[0:3])                 // abc
	fmt.Println(strCn[0:3])                 // 中
	fmt.Println(string([]rune(strCn)[0:3])) // 中文測

func SubString(str string, begin, length int) (substr string) {
	// 將字符串的轉換成[]rune
	rs := []rune(str)
	lth := len(rs)
	// 簡單的越界判斷
	if begin < 0 {
		begin = 0
	}
	if begin >= lth {
		begin = lth
	}
	end := begin + length
	if end > lth {
		end = lth
	}
	// 返回子串
	return string(rs[begin:end])
}
	fmt.Println(SubString("中文測試", 1, 3)) // 文測試
	fmt.Println(SubString("abcd", 1, 3)) // bcd

【中文字符串定位】

// 思路:首先經過strings庫中的Index函數得到子串的字節位置,再經過這個位置得到子串以前的字節數組pre,
// 再將pre轉換成[]rune,得到[]rune的長度,即是子串以前字符串的長度,也就是子串在字符串中的字符位置
// 這裏用的是string.Index函數,相似的,也能夠寫中文字符串的相似strings中的IndexAny,LastIndex等函數
func UnicodeIndex(str, substr string) int {
	// 子串在字符串的字節位置
	result := strings.Index(str, substr)
	if result >= 0 {
		// 得到子串以前的字符串並轉換成[]byte
		prefix := []byte(str)[0:result]
		// 將子串以前的字符串轉換成[]rune
		rs := []rune(string(prefix))
		// 得到子串以前的字符串的長度,即是子串在字符串的字符位置
		result = len(rs)
	}

	return result
}
	fmt.Println(UnicodeIndex("中文測試", "文")) // 1
	fmt.Println(UnicodeIndex("abcd", "c")) // 2

【使用"`"定義不作轉義處理的原始字符串,支持跨行】

s := `a
b\r\n\x00
	c`
	println(s)

輸出

a
b\r\n\x00
	c

注意上面第二行是頂行寫的,否則縮進也是會如實反映的。


鏈接跨行字符串時,"+" 必須在上一行末尾,不然致使編譯錯誤

s := "hello, " +
		"world!"
	s2 := "hello, "
	+"World." // invalid operation: + untyped string

【字符串和其餘類型的轉換】

strconv包。


  • 不能用序號獲取字節元素指針,&s[i]非法
  • 不可變類型,沒法修改字節數組
  • 字節數組尾部不包含NULL

runtime.h

struct String
	{
		byte* str;
		intgo len;
	};

中文字符串長度

unicode/utf8 包中有一個RuneCountInString()

bytes包

  • bytes 包和字符串包十分相似,並且它還包含一個十分有用的類型 Buffer。這是一個 bytes 的定長 buffer,提供 Read 和 Write 方法,由於讀寫不知道長度的 bytes 最好使用 buffer。
  • Buffer 能夠這樣定義:var buffer bytes.Buffer 或者 new 出一個指針:var r *bytes.Buffer = new(bytes.Buffer) 或者經過函數:func NewBuffer(buf []byte) *Buffer,這就建立了一個 Buffer 對象而且用 buf 初始化好了;NewBuffer 最好用在從 buf 讀取的時候使用。
  • 經過 buffer 串聯字符串:相似於 Java 的 StringBuilder 類 建立一個 Buffer,經過 buffer.WriteString(s) 方法將每一個 string s 追加到後面,最後再經過 buffer.String() 方法轉換爲 string,下面是代碼段:
var buffer bytes.Buffer
for {
    if s, ok := getNextString(); ok { //method getNextString() not shown here
        buffer.WriteString(s)
    } else {
        break
    }
}
fmt.Print(buffer.String(), "\n")

這種實現方式比使用 += 要更節省內存和 CPU,尤爲是要串聯的字符串數目特別多的時候。

字符類型

byte和rune

byte(其實是uint8的別名),表明UTF-8字符串的單個字節的值

rune(int32),表明單個Unicode字符

var abc byte
	abc = 'a'
	fmt.Print(abc) // 97

若是abc = ‘你’,會提示超出byte的範圍

var abc rune
	abc = '你'
	fmt.Print(abc) // 20320

字符數組

string => []byte

[]byte("hello")

[]byte => string

string([]byte)

可以使用的庫:bytes:

bytes.NewBuffer()

rune可作變量名

rune := rune('a')
	fmt.Println(rune) // 97

十六進制

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

(\x 老是緊跟着長度爲 2 的 16 進制數)

另一種可能的寫法是 \ 後面緊跟着長度爲 3 的十進制數,例如:\377

通常使用格式 U+hhhh 來表示,其中 h 表示一個 16 進制數。其實 rune 也是 Go 當中的一個類型,而且是 int32 的別名。

在書寫 Unicode 字符時,須要在 16 進制數以前加上前綴 \u 或者 \U。

由於 Unicode 至少佔用 2 個字節,因此咱們使用 int16 或者 int 類型來表示。若是須要使用到 4 字節,則會加上 \U 前綴;前綴 \u 則老是緊跟着長度爲 4 的 16 進制數,前綴 \U 緊跟着長度爲 8 的 16 進制數。

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

一些常見的函數見unicode包

數組

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

數組長度在定義後就不可更改,在聲明時長度能夠爲一個常量或者一個常量表達式(常量表達式是指在編譯期便可計算結果的表達式)。


【數組長度】

var arr [10]byte
len(arr) // 10

var arr [10][23]int
fmt.Println(len(arr)) // 10

【初始化】

arr := [5]int{1, 2, 3, 4, 5}
fmt.Println(arr[4]) // 5
b := [10]int{1, 2, 3}

聲明瞭一個長度爲10的int數組,其中前三個元素初始化爲一、二、3,其它默認爲0

若是不想寫[5],也可使用[...]代替:

arr := [...]int{1, 2, 3, 4, 5}

也能夠省略,什麼都不寫(這是數組仍是切片?這是切片)

arr := []int{1, 2, 3}

指定索引來初始化

var names = []string{
		1: "a",
		2: "b",
		4: "d",
	}

names[0]和names[3]都是""


數組指針

arr1 := new([3]int)
	fmt.Printf("arr1 type:%T\n", arr1) // arr1 type:*[3]int
	arr2 := [3]int{}
	fmt.Printf("arr2 type:%T\n", arr2) // arr2 type:[3]int
	fmt.Println(arr1, arr2) // &[0 0 0] [0 0 0]

	arr1[1] = 4
	(*arr1)[2] = 1
	arr2[1] = 5
	fmt.Println(arr1, arr2) // &[0 4 1] [0 5 0]

【遍歷】

方式一:

arr := [5]int{1, 3, 5, 7, 9}
	for i, n := 0, len(arr); i < n; i++ {
		fmt.Println(i, "=>", arr[i])
	}

方式二:

arr := [5]int{1, 3, 5, 7, 9}
	for i, v := range arr {
		fmt.Println(i, "=>", v)
	}

方式三:(range匿名變量)

for i, v := range [5]int{1, 2, 3, 4} {
		fmt.Println(i, "=>", v)
	}

【數組是值類型】

數組是一個值類型(value type)。全部的值類型變量在賦值和做爲參數傳遞時都將產生一次複製動做。若是將數組做爲函數的參數類型,則在函數調用時該參數將發生數據複製。所以,在函數體中沒法修改傳入的數組的內容,由於函數內操做的只是所傳入數組的一個副本。

func main() {
	arr := [5]int{1, 2, 3, 4, 5}
	modify(arr)
	fmt.Println("in main() arr is: ", arr)
}

func modify(arr [5]int) {
	arr[0] = 10
	fmt.Println("in modify() arr is: ", arr)
}

輸出結果:

in modify() arr is:  [10 2 3 4 5]
in main() arr is:  [1 2 3 4 5]

固然,也能夠給函數傳遞一個數組的指針:

func sum(a *[3]float64) (sum float64) {
	for _, v := range *a {
		sum += v
	}
	return
}
	arr := [...]float64{7.0, 5.4, 9.2}
	fmt.Println(sum(&arr))

不過,這種風格並不符合Go的語言習慣。相反的,應該使用切片。

優化

count[x] = count[x] * scale

可替換成

count[x] *= scale

這樣能夠省去對變量表達式的重複計算

多維數組

// 聲明瞭一個二維數組,該數組以兩個數組做爲元素,其中每一個數組中又有4個int類型的元素
	doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}

	// 上面的聲明能夠簡化,直接忽略內部的類型
	easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}

	arr := [3][2]int{
		{1, 2},
		{3, 4},
		{5, 6}} // 若是最後的}放到下一行了,則須要使用,({5,6},)
	fmt.Println(arr[2]) // [5 6]

	towDimen := [][]string{
		[]string{"a", "b", "c"},
		[]string{"d", "e"},
	}

數組切片

數組切片的數據結構能夠抽象爲如下3個變量:

  • 一個指向原生數組的指針;
  • 數組切片中的元素個數;
    • 數組切片已分配的存儲空間

切片持有對底層數組的引用,若是你將一個切片賦值給另外一個,兩者都將引用同一個數組。若是函數接受一個切片參數,那麼其對切片的元素所作的改動,對於調用者是可見的,比如是傳遞了一個底層數組的指針。所以,Read函數能夠接受一個切片參數,而不是一個指針和一個計數;切片中的長度已經設定了要讀取的數據的上限.

注意 絕對不要用指針指向 slice。slice 自己已是一個引用類型,因此它自己就是一個指針!!


【建立切片】

方式一:在已有數組的基礎上建立

arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:4]   // 前4個元素 0-3
fmt.Println(slice) // [1 2 3 4]

slice := arr[1:] // 從1開始到結尾 [2 3 4 5]

slice := arr[1:4] // 1-3 [2 3 4]

slice := arr[:] // 所有
  • 第一個數能夠是0到len(arr),包括len(arr),此時切片是空的[],其他數則會報錯
  • 第二個數能夠是0到len(arr),0時切片爲[],其他報錯
  • 若是切arr[0:0]則切片len是0

切片的改變可影響原數組

arr := [3]int{1, 2, 3}
	slice := arr[:]
	slice[0] = 5
	fmt.Printf("arr=%v, slice=%v", arr, slice) // arr=[5 2 3], slice=[5 2 3]

方式二:直接建立

建立一個初始元素個數爲5的數組切片,元素初始值爲0,cap爲5:

mySlice1 := make([]int, 5)

建立一個初始元素個數爲5的數組切片,元素初始值爲0,並預留10個元素的存儲空間:

mySlice2 := make([]int, 5, 10)

直接建立並初始化包含5個元素的數組切片(len和cap都爲5):

mySlice3 := []int{1, 2, 3, 4, 5}

固然,事實上還會有一個匿名數組被建立出來,只是不須要咱們來操心而已

如下這兩種方式可建立相同的slice:

s1 := make([]int, 5, 10)
	s2 := new([10]int)[:5]
	fmt.Printf("s1 type:%T, s2 type:%T\n", s1, s2) // s1 type:[]int, s2 type:[]int
	fmt.Printf("s1 len:%d,cap:%d, s2 len:%d,cap:%d\n", len(s1), cap(s1), len(s2), cap(s2))
	// s1 len:5,cap:10, s2 len:5,cap:10

方法三:基於切片建立切片

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

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

然而在這裏 oldSlice 的容量就是5,而 newSlice 的容量也是5,由於這倆用得是同一個數組。

這個和從新分片是同樣的。

由於字符串是純粹不可變的字節數組,它們也能夠被切分紅 slice

s := "hello,世界"
	s2 := s[6:9]
	fmt.Println(s2) // 世

【切片的第3個參數】

slice := [...]int{0, 1, 2, 3, 4, 5, 6}
	fmt.Println(slice)
	s := slice[1:2:4]
	fmt.Println(len(s), cap(s)) // 1 3
	s1 := slice[1:2]
	fmt.Println(len(s1), cap(s1)) // 1 6

第3個參數表示容量最大界索引,第三個參數減去第一個參數的差值就是容量。


【查看長度和容量】

slice := []int{1, 2, 3, 4, 5}
	fmt.Println(len(slice), cap(slice)) // 5 5

	slice2 := make([]int, 5, 10)
	fmt.Println(len(slice2), cap(slice2)) // 5 10

一個 slice s 能夠這樣擴展到它的大小上限:s = s[:cap(s)]

s := make([]int, 5, 10)
	s = s[:cap(s)]
	fmt.Println(len(s), cap(s)) // 10,10

【從新分片】

arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
	s := arr[:5]
	fmt.Println(s) // [1 2 3 4 5]
	s = arr[5:10]
	fmt.Println(s) // [6 7 8 9 0]

【新增元素】

slice := []int{1, 2, 3, 4, 5}
	slice2 := append(slice, 6, 7, 8)
	fmt.Println(slice) // [1 2 3 4 5]
	fmt.Println(slice2) // [1 2 3 4 5 6 7 8]

append()是產生了一個新的切片

append()的第二個參數實際上是一個不定參數,咱們能夠按本身需求添加若干個元素,甚至直接將一個數組切片追加到另外一個數組切片的末尾:

slice := []int{1, 2, 3, 4, 5}
	slice2 := []int{8, 9, 10}
	slice = append(slice, slice2...)
	fmt.Println(slice)

第二個參數slice2後面加了三個點,即一個省略號,若是沒有這個省略號的話,會有編譯錯誤,由於按append()的語義,從第二個參數起的全部參數都是待附加的元素。由於slice中的元素類型爲int,因此直接傳遞slice2是行不通的。加上省略號至關於把slice2包含的全部元素打散後傳入。 上述調用等同於:

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

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

append函數會改變slice所引用的數組的內容,從而影響到引用同一數組的其它slice。 但當slice中沒有剩餘空間(即(cap-len) == 0)時,此時將動態分配新的數組空間。返回的slice數組指針將指向這個空間,而原數組的內容將保持不變;其它引用此數組的slice則不受影響

append interface{}時的坑

var slice []interface{} = make([]interface{}, 0)
	slice = append(slice, "hello")
	slice = append(slice, 12)

	var arr []interface{} = []interface{}{"one", "two"}
	slice = append(slice, arr...)

	var arr2 []string = []string{"three", "four"}
	slice = append(slice, arr2...) // cannot use arr2 (type []string) as type []interface {} in append

	fmt.Println(slice)

這裏在append arr2時是想讓go作一個隱式轉換,把[]string轉化成[]interface{},但顯然go如今還不支持。

數組是nil時依然能夠append

var arr []string = nil
	arr = append(arr, "hello")

並不會報空指針異常,可能仍是由於類型和值都爲nil時才爲nil,很明顯,這裏的類型不爲nil。


【內容複製】

數組切片支持Go語言的另外一個內置函數copy(),用於將內容從一個數組切片複製到另外一個數組切片。若是加入的兩個數組切片不同大,就會按其中較小的那個數組切片的元素個數進行復制

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

【刪除】

func (m *MusicManager) Remove(index int) *MusicEntry {
	if index < 0 || index >= len(m.musics) {
		return nil
	}
	removedMusic := m.musics[index] // 這裏不能加&,不然刪除的時候會被後面的覆蓋,因此須要把元素複製出來而不是隻取地址
	// 從切片中刪除
	len := len(m.musics)
	if len == 0 || len == 1 { // 刪除僅有的一個
		m.musics = m.musics[0:0]
	} else if index == 0 { // 以後的長度至少爲2
		// 刪除開頭的
		m.musics = m.musics[1:]
	} else if index == len { //最後一個
		m.musics = m.musics[:len-1]
	} else { // 中間的,長度至少爲3
		m.musics = append(m.musics[:index], m.musics[index+1:]...)
	}
	return &removedMusic
}

[start : end]操做並無改變底層的數組,僅僅是改變了開始索引和長度,append操做會覆蓋掉中間的元素,但底層數組仍是沒有改變,只是改變了索引位置和長度。

通常性的代碼:

func DeleteString(slice []string, index int) []string {
	len := len(slice)
	if index < 0 || index >= len {
		return slice
	}
	if len == 0 {
		return slice
	}
	if len == 1 {
		return slice[0:0]
	}
	if index == 0 {
		return slice[1:]
	}
	if index == len-1 {
		return slice[:len-1]
	}
	return append(slice[:index], slice[index+1:]...)
}

上面代碼有些囉嗦了,下面是簡潔版的:

func DeleteString2(slice []string, index int) []string {
	if index < 0 || index >= len(slice) {
		return slice
	}
	return append(slice[:index], slice[index+1:]...)
}

二維切片

Go的數組和切片都是一維的。要建立等價的二維數組或者切片,須要定義一個數組的數組或者切片的切片,相似這樣:

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte     // A slice of byte slices.

由於切片是可變長度的,因此能夠將每一個內部的切片具備不一樣的長度。這種狀況很常見,正如咱們的LinesOfText例子中:每一行都有一個獨立的長度。

text := LinesOfText{
	[]byte("Now is the time"),
	[]byte("for all good gophers"),
	[]byte("to bring some fun to the party."),
}

有時候是須要分配一個二維切片的,例如這種狀況可見於當掃描像素行的時候。有兩種方式能夠實現。一種是獨立的分配每個切片;另外一種是分配單個數組,爲其 指定單獨的切片們。使用哪種方式取決於你的應用。若是切片們可能會增大或者縮小,則它們應該被單獨的分配以免覆寫了下一行;若是不會,則構建單個分配 會更加有效。做爲參考,這裏有兩種方式的框架。首先是一次一行:

// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
	picture[i] = make([]uint8, XSize)
}

而後是分配一次,被切片成多行:

// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
	picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

map

key能夠爲任何定義了等於操做符的類型,例如整數,浮點和複數,字符串,指針,接口(只要其動態類型支持等於操做),結構體和數組。切片不能 做爲map的key,由於它們沒有定義等於操做。和切片相似,map持有對底層數據結構的引用。若是將map傳遞給函數,其對map的內容作了改變,則這 些改變對於調用者是可見的。

key能夠是任意數據類型,只要該類型可以用==來進行比較

map和其餘基本型別不一樣,它不是thread-safe,在多個go-routine存取時,必須使用mutex lock機制


【變量聲明】

var person map[string] string

[]內是鍵的類型,後面是值類型


【建立】

person = make(map[string]string)

聲明加+建立:

var person map[string]string = make(map[string]string)
	person := make(map[string]string)

指定該map的初始存儲能力:

person = make(map[string]string, 100)

建立並初始化map:

var person map[string]string
	person = map[string]string{
		"a": "haha",
		"b": "ni", // 最後的逗號是必須的
	}

【元素賦值/添加元素】

var person map[string]string = make(map[string]string)
	person["1"] = "abc"
m := make(map[string]int)
m["a"]++

不用擔憂map在沒有當前的key時就對其進行++操做會有什麼問題,由於go語言在碰到這種狀況時,會自動將其初始化爲0,而後再進行操做。


【元素刪除】

delete(person, "1")

若是傳入的map是nil,則報錯,若是鍵不存在,則什麼都不發生


【元素查找】

value, ok := person["2"]
	if ok {
		fmt.Println(value)
	} else {
		fmt.Println("does not find!")
	}

或者:

if value, ok := person["2"]; ok {
	fmt.Println(value)
} else {
	fmt.Println("does not find!")
}

即使是nil也是能夠查找的:

var m map[string]int = nil
	if v, ok := m["a"]; ok {
		fmt.Println(v)
	} else {
		fmt.Println("!ok")
	}
	// 輸出!ok

map中的元素不會出現nil的現象(很神奇):

func main() {
	m := make(map[string]entry)
	fmt.Println(m["a"].name == "") // true
}

type entry struct {
	name string
}

m[「a」]返回的並非nil,而是{},若是map的元素是指針,則是nil

m := make(map[string]*entry)
	fmt.Println(m["a"] == nil) // true

【遍歷】

for k, v := range person {
		fmt.Println("key=", k, "value=", v)
	}

map也是一種引用類型,若是兩個map同時指向一個底層,那麼一個改變,另外一個也相應的改變:

m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut"  // 如今m["hello"]的值已是Salut了

【清空map】

for k, _ := range m {
		delete(m, k)
	}

或者從新賦值:

m = make(map[string]string)

【線程安全的map】

【map中的對象是個拷貝問題】

直接對map對象使用[]操做符得到的對象不能直接修改狀態:

type Person struct {
		age int
	}
	m := map[string]Person{"c": {10}}
	m["c"].age = 100 // 編譯錯誤:cannot assign to m["c"].age

經過查詢map得到的對象是個拷貝,對此對象的修改不影響原有對象的狀態:

type Person struct {
		age int
	}
	m := map[string]Person{"c": {10}}
	p := m["c"]
	p.age = 20
	fmt.Println(p.age)      // 20
	fmt.Println(m["c"].age) // 10

解決方法

  1. map中存儲指針而不是結構體
type Person struct {
		age int
	}
	m := map[string]*Person{"c": {10}}
	p := m["c"]
	p.age = 20
	fmt.Println(p.age)      // 20
	fmt.Println(m["c"].age) // 20
  1. 修改了對象狀態之後從新加到map裏
type Person struct {
		age int
	}
	m := map[string]Person{"c": {10}}
	p := m["c"]
	p.age = 20
	fmt.Println(p.age) // 20
	m["c"] = p
	fmt.Println(m["c"].age) // 20

【分拆map】

分拆map,提升併發能力

Go數據底層的存儲

下面這張圖來源於Russ Cox Blog中一篇介紹Go數據結構的文章,你們能夠看到這些基礎類型底層都是分配了一塊內存,而後存儲了相應的值。

指針

  • 在 32 位機器上佔用 4 個字節,在 64 位機器上佔用 8 個字節,而且與它所指向的值的大小無關。
  • 整形的指針
var p *int = new(int)
	*p = 5
	fmt.Println(*p, p) // 5 0xc0820001d0

	q := 6
	var pq *int = &q
	fmt.Println(*pq, pq) // 6 0xc082000200
  • 不能獲得文字或常量的地址
const i = 5
ptr := &i //error: cannot take the address of i
ptr2 := &10 //error: cannot take the address of 10
  • 對一個空指針的反向引用是不合法的,而且會使程序崩潰:
package main
func main() {
    var p *int = nil
    *p = 0
}
// in Windows: stops only with: <exit code="-1073741819" msg="process crashed"/>
// runtime error: invalid memory address or nil pointer dereference
  • 能夠在 unsafe.Pointer 和任意類型指針間進行轉換
x := 0x12345678
	p := unsafe.Pointer(&x) // *int -> Pointer
	n := (*[4]byte)(p)      // Pointer -> *[4]byte,  3,5也能夠

	for i := 0; i < len(n); i++ {
		fmt.Printf("%X ", n[i])
	}

輸出:

78 56 34 12
  • 返回局部變量指針是安全的,編譯器會根據須要將其分配在 GC Heap 上
func test() *int {
		x := 100
		return &x // 在堆上分配x內存,但在內聯時,也能夠直接分配在目標棧
	}
  • 將 Pointer 轉換成 uintptr, 可變相實現指針運算
d := struct {
		s string
		x int
	}{"abc", 100}

	p := uintptr(unsafe.Pointer(&d)) // *struct -> Pointer -> uintptr
	p += unsafe.Offsetof(d.x)        // uintptr + offset
	p2 := unsafe.Pointer(p)          // uintptr -> Pointer
	px := (*int)(p2)                 // Pointer -> *int
	*px = 200                        // d.x = 200
	fmt.Printf("%#v\n", d)

輸出:

struct { s string; x int }{s:"abc", x:200}

注意:GC 把 uintptr 當成普通整數對象,它沒法阻止"關聯"對象被回收。

相關文章
相關標籤/搜索