Go 语言学习笔记(一):基础知识
目錄
語言簡介
初識 Go 程序
Go 詞法單元
變量和常量
復合數據類型
語言簡介
已經有那么多種編程語言了,為什么還要發明新語言?為什么還要去學習新語言?相信不少人都有這樣的疑問。答案很簡單,雖然有那么多種語言,但每種語言都有其獨特的應用領域,在某個領域使用某種語言能達到收益/投入的最大化。比如在嵌入式領域,匯編和 C 是首選;在操作系統領域,C 是首選;在系統級服務編程領域,C++ 是首選;在企業級應用程序和 Web 應用領域,Java 是首選。就好比木工的工具箱中錘子可以有很多種,大廚的工具箱中刀子有很多種一樣,某種語言就像某種錘子或者某種刀一樣,有其特別應用的領域 。
?
Go 語言的誕生主要基于如下原因 :
(1) 摩爾定律接近失效后多核服務器己經成為主流,當前的編程語言對并發的支持不是很好,不能很好地發揮多核 CPU 的威力。
(2) 程序規模越來越大,編譯速度越來越慢,如何快速地編譯程序是程序員的迫切需求。
(3) 現有的編程語言設計越來越復雜,由于歷史的包袱,某些特性的實現不怎么優雅,程序員花費了更多的精力來應對編程語法細節而不是問題域。
Go 語言就是為了解決當下編程語言對?并發支持不友好、編譯速度慢、編程復雜?這三個問題而誕生的 。
?
?
初識 Go 程序
package mainimport "fmt"func main() {/* 這是我的第一個簡單的程序 */fmt.Println("Hello, World!") }Linux環境下編譯 go 程序的方法:go build asd.go,然后使用 ./asd 運行相應的程序。或者直接使用 go run asd.go 運行程序。
讓我們來看下以上程序的各個部分:
(1) 第 1 行代碼?package main?定義了包名。你必須在源文件中非注釋的第一行指明這個文件屬于哪個包,如:package main。package main表示一個可獨立執行的程序,每個 Go 應用程序都包含一個名為 main 的包。
(2) 第 3 行?import "fmt"?告訴 Go 編譯器這個程序需要使用 fmt 包,fmt 包實現了格式化 IO 的函數。
(3) 第 5 行?func main()?是程序開始執行的函數。main 函數是每一個可執行程序所必須包含的,一般來說都是在啟動后第一個執行的函數(如果有 init() 函數則會先執行該函數)。
(4) 第?7 行?fmt.Println(...)?可以將字符串輸出到控制臺,并在最后自動增加換行字符 \n。使用 fmt.Print("hello, world\n") 可以得到相同的結果。Print 和 Println 這兩個函數也支持使用變量,如:fmt.Println(arr)。如果沒有特別指定,它們會以默認的打印格式將變量輸出到控制臺。
當標識符(包括常量、變量、類型、函數名、結構字段等等)以一個大寫字母開頭,如:Group1,那么使用這種形式的標識符的對象就可以被外部包的代碼所使用(客戶端程序需要先導入這個包),這被稱為導出(像面向對象語言中的 public);標識符如果以小寫字母開頭,則對包外是不可見的,但是他們在整個包的內部是可見并且可用的(像面向對象語言中的 protected)。
在 Go 程序中,一行代表一個語句結束。每個語句不需要像 C 家族中的其它語言一樣以分號 ; 結尾,因為這些工作都將由 Go 編譯器自動完成。如果你打算將多個語句寫在同一行,則它們必須使用 ; 人為區分,但在實際開發中我們并不鼓勵這種做法。
?
Go 代碼的特征解讀
(1) 源程序以 .go 為后綴。
(2) 源程序默認為 UTF-8 編碼。
(3) 標識符區分大小寫,大寫字母開頭的標識符可以被外部包的代碼所使用。
(4) 語句結尾的分號可以省略。
(5) 函數 func?開頭,函數體開頭的?"?{?"?必須在函數頭所在行尾部,不能單獨起一行,否則會報錯。
(6) 宇符串字面量使用?"?"(雙引號)括起來。
(7) 調用包里面的方法通過點?" . "?訪問符,比如示例中的 fmt.Println。
(8) main 函數所在的包名必須是 main。
?
?
Go 詞法單元
在介紹 Go 語言具體語法之前,先介紹一下現代高級語言的源程序內部的幾個概念: token、關鍵字、標識符、操作符、分隔符和字面量。
token
token 是構成源程序的基本不可再分割的單元。編譯器編譯源程序的第一步就是將源程序分割為一個個獨立的 token,這個過程就是詞法分析。Go?語言的 token 可以分為關鍵字、標識符、操作符、分隔符和字面常量等,分類如圖所示。
Go 語言里面的 token 是怎么分割的?Go 的 token 分隔符有兩類:一類是?操作符,還有一類自身沒有特殊含義,僅用來分隔其他 token,被稱為?純分隔符。
操作符:操作符就是一個天然的分隔符,同時其自身也是一個 token,語句如下所示: sum := a+b
其中," := "?和?" + "?既是分隔符,也是 token,所以這個簡單的語句被分割為 5 個 token:"?sum "、" := "、"? a "、" + "、" b ",Go 語言操作符的相關詳細內容后面會涉及。
純分隔符:其本身不具備任何語法含義,只作為其他 token 的分割功能。包括空格、制表符、換行符和回車符,多個相鄰的空格或者制表符會被編譯器看作分隔符處理,例如:package main。
這是一個包聲明的語句,package 和 main 之間可以有任意多個空格或者制表符,Go 編譯器會將其作為一個分隔符處理,最后分離出來兩個 token : package 和 main。
?
?
標識符
編程語言的標識符用來標識變量、類型、常量等語法對象的符號名稱,其在語法分析時作為一個 token 存在。編程語言的標識符總體上分為兩類:一類是語言設計者預留的標識符, 一類是編程者可以自定義的標識符。前者一般由語言設計者確定,包括語言的預聲明標識符及用于后續語言擴展的保留字;后者是用戶在編程過程中自行定義的變量名、常量名、函數名等一切符合語言規范的標識符。有一點需要注意,用戶自定義的標識符不應該使用語言設計者的預留標識符,這可能導致歧義,并嚴重影響代碼的可讀性。
Go 的標識符構成規則是:開頭第一個字符必須是字母或下劃線,后面跟任意多個字符、數字或下劃線,并且區分大小寫,Unicode?字符也可以作為標識符的構成,但是一般不推薦這么使用。我們在定義新的標識符時要避開 Go 語言預聲明標識符,以免引起混亂。
Go 語言預聲明的標識符包括關鍵字、內置數據類型標識符、常量值標識符、內置函數和空白標識符。在寫 Go 源程序的過程中,用戶自定義標識符用在包名、函數名、自定義類型名、變量名和常量名等上。
?
?
關鍵字
編程語言里面的關鍵字是指語言設計者保留的有特定語法含義的標識符,這些關鍵字有自己獨特的用途和語法含義,它們一般用來控制程序結構,每個關鍵字都代表不同語義的語法糖。
語法糖(Syntactic sugar),也譯為糖衣語法,是由英國計算機科學家彼得·約翰·蘭達(Peter J. Landin)發明的一個術語,指計算機語言中添加的某種語法,這種語法對語言的功能并沒有影響,但是更方便程序員使用。通常來說使用語法糖能夠增加程序的可讀性,從而減少程序代碼出錯的機會。舉個例子:在 C 語言里用 a[i] 表示 *(a+i),由此可見語法糖不是 "現代語言" 獨有,這種寫法簡潔明了,容易被人理解。
Go 語言是一門極簡的語言,只有如下 25 個關鍵字 :
這 25 個關鍵字按照功能又可以分為三個部分:
引導程序整體結構的 8 個關鍵字
package ?? 定義包名的關鍵字 import ????導入包名關鍵字 const ?????常量聲明關鍵字 var ???????變量聲明關鍵字 func ??????函數定義關鍵字 defer ?????延遲執行關鍵字 go ????????并發語法糖關鍵字 return ????函數返回關鍵字?
聲明復合數據結構的 4 個關鍵字
struct ????定義結構類型關鍵字 interface ?定義接口類型關鍵字 map ?????? 聲明或創建map類型關鍵字 chan ????? 聲明或創建通道類型關鍵字?
控制程序結構的 13 個關鍵字
if else ???????? if else語句關鍵字 for range break continue ?? for 循環使用的關鍵字 switch select type case default fallthrough ?? switch 和 select 語句使用的關鍵字 goto ??? goto 跳轉語句?
內置數據類型標識符
豐富的內置類型支持是高級語言的基本特性,基本類型也是構造用戶自定義類型的基礎。為了標識每種內置數據類型,Go 定義了一套預聲明標識符,這些標識符用在變量或常量聲明時。Go 語言內置了 20 個預聲明數據類型標識符。Go 語言按類別有以下幾種數據類型:
布爾型:bool 整型:byte、int、int8、int16、int32、int64、uint、unint8、uint16、uint32、uint64、uintprt 浮點型:float32、float64 復數:complex64、complex128 錯誤類型:error 字符和字符串型:rune、stringGo 是一種強類型靜態編譯型語言,在定義變量和常量時需要顯式地指出數據類型,當然 Go 也支持自動類型推導,在聲明初始化內置類型變量時,Go 可以自動地進行類型推導。但是在定義新類型或函數時,必須顯式地帶上類型標識符。?
?
內置函數
make、new、len、cap、append、copy、delete、panic、recover、close、complex、real、image、print、println。內置函數也是高級語言的一種語法糖,由于其是語言內置的,不需要用 import 引入,內置函數具有全局可見性。注意到其中有以小寫字母開頭的,但是并不影響其全局可用性。
?
常量值標識符
true false ? true 和 false 表示 bool 類型的兩常量值:真和假 iota ? 用在連續的枚舉類型的聲明中 nil ?? 指針/引用型的變量的默認值就是 nil??
空白標識符
_ ,空白標識符有特殊的含義,用來聲明-個匿名的變量,該變量在賦值表達式的左端,空白標識符引用通常被用作占位,比如忽略函數多個返回值中的一個和強制編譯器做類型檢查。
?
Go 的源程序基本構成:
(1) 關鍵字引導程序的基本結構。
(2) 內置類型標識符輔助聲明變量和常量。
(3) 字面量輔助變量和常量的初始化。
(4) 分隔符幫助 Go 編譯器識別各個 token。
(5) 操作符、變量和關鍵字一起構成豐富的語法單元。
?
?
變量和常量
變量
變量使用一個名稱來綁定一塊內存地址,該內存地址中存放的數據類型由定義變量時指定的類型決定,該內存地址里面存放的內容可以改變。Go 語言變量聲明:
(1) 顯式的完整聲明
var varName dataType = value 例如: var a int = 1 var a int = 2*3 var a int =?b關鍵字?var 用于變量聲明,varName 是變量名標識符,dataType 是基本類型。value 是變量的初始值,初始值可以是字面量,也可以是其他變量名,還可以是一個表達式;如果不指定初始值,則 Go 默認將該變量初始化為類型的零值。Go 的變量聲明后就會立即為其分配空間。
?
(2) 根據值自動判斷變量類型
var v_name = valuepackage main import "fmt" func main() {var d = truefmt.Println(d) }?
(3) 短類型聲明
varName := valuepackage main import "fmt" func main() {f := "Runoob" // var f string = "Runoob"fmt.Println(f) }注意:" := "?聲明只能出現在函數內,而不可以用于全局變量的聲明與賦值。:=?左側如果沒有聲明新的變量,就產生編譯錯誤。
var intVal int intVal :=1 // 這時候會產生編譯錯誤 intVal,intVal1 := 1,2 // 此時不會產生編譯錯誤,因為有聲明新的變量,因為 := 是一個聲明語句package main import "fmt" func main() {f := "Runoob" // var f string = "Runoob"fmt.Println(f) }?
多變量聲明
類型相同多個變量, 非全局變量 var vname1, vname2, vname3 type vname1, vname2, vname3 = v1, v2, v3var vname1, vname2, vname3 = v1, v2, v3 和 python 很像,不需要顯示聲明類型,自動推斷 vname1, vname2, vname3 := v1, v2, v3 出現在 := 左側的變量不應該是已經被聲明過的,否則會導致編譯錯誤這種因式分解關鍵字的寫法一般用于聲明全局變量 var (vname1 v_type1vname2 v_type2 )多變量可以在同一行進行賦值,如: var a, b, c int var c string a, b, c = 5, 7, "abc" package mainvar x, y int var ( // 這種因式分解關鍵字的寫法一般用于聲明全局變量a intb bool )var c, d int = 1, 2 var e, f = 123, "hello"//這種不帶聲明格式的只能在函數體中出現 //g, h := 123, "hello"func main(){g, h := 123, "hello"println(x, y, a, b, c, d, e, f, g, h) }如果函數體內的一個值聲明了但是沒有使用,會報錯(declared and not used)。只有個使用了那個值后,錯誤才會移除。但是全局變量是允許聲明但不使用的。例如:下面的代碼只會報 a?declared and not used 的錯,而 testasd 不會影響程序。
package mainimport "fmt" var testasd intfunc main() {var a string = "abc"fmt.Println("hello, world") }?
空白標識符在函數返回值時的使用:
package mainimport "fmt"func main() {_, numb, strs := numbers() //只獲取函數返回值的后兩個fmt.Println(numb,strs) }//一個可以返回多個值的函數 func numbers()(int,int,string){a , b , c := 1 , 2 , "str"return a,b,c }?
?
常量
常量使用一個名稱來綁定一塊內存地址,該內存地址中存放的數據類型由定義常量時指定的類型決定,而且該內存地址里面存放的內容不可以改變。Go 中常量分為布爾型、宇符串型和數值型常量。常量存儲在程序的只讀段里(.rodata section)。
常量的定義格式:const identifier [type] = value,你可以省略類型說明符 [type],因為編譯器可以根據變量的值來推斷其類型。
顯式類型定義:?const b string = "abc" 隱式類型定義:?const b = "abc" package mainimport "fmt"func main() {const LENGTH int = 10const WIDTH int = 5 var area intconst a, b, c = 1, false, "str" //多重賦值area = LENGTH * WIDTHfmt.Printf("面積為 : %d", area)println()println(a, b, c) }?
常量作枚舉
const (Unknown = 0Female = 1Male = 2 )常量可以用 len(),cap(),unsafe.Sizeof() 函數計算表達式的值。常量表達式中,函數必須是內置函數,否則編譯不過:
package mainimport "unsafe" const (a = "abc"b = len(a)c = unsafe.Sizeof(a) )func main(){println(a, b, c) }?
iota
iota,特殊常量,可以認為是一個可以被編譯器修改的常量。iota 在 const 關鍵字出現時將被重置為 0(const 內部的第一行之前),const 中每新增一行常量聲明將使 iota 計數一次(iota 可理解為 const 語句塊中的行索引)。
package mainimport "fmt"func main() {const (a = iota //0b //1c //2d = "ha" //獨立值,iota += 1e //"ha" iota += 1f = 100 //iota +=1g //100 iota +=1h = iota //7,恢復計數i //8)fmt.Println(a,b,c,d,e,f,g,h,i) }輸出:0 1 2 ha ha 100 100 7 8?
?
變量的作用域
作用域為已聲明標識符所表示的常量、類型、變量、函數或包在源代碼中的作用范圍。Go 語言中變量可以在三個地方聲明:函數內定義的變量稱為局部變量、函數外定義的變量稱為全局變量、函數定義中的變量稱為形式參數。
package mainimport "fmt"/* 聲明全局變量 */ var a int = 20;func main() {/* main 函數中聲明局部變量 */var a int = 10var b int = 20var c int = 0fmt.Printf("main()函數中 a = %d\n", a);c = sum( a, b);fmt.Printf("main()函數中 c = %d\n", c); }/* 函數定義-兩數相加 */ func sum(a, b int) int {fmt.Printf("sum() 函數中 a = %d\n", a);fmt.Printf("sum() 函數中 b = %d\n", b);return a + b; }?
?
字符串
Go 語言將字符串作為一種原生的基本數據類型, 字符串的初始化可以使用字符串字面量。例如 :var a = "hello, world" 。
(1) 字符串是常量,可以通過類似數組的索引訪問其字節單元,但是不能修改某個字節的值。例如 :
var a = "hello, world" b : = a [0] a[1] = 'a ' //error(2) 字符串轉換為切片?[ ]byte(s)?要慎用,尤其是當數據量較大時(每轉換一次都需復制內容)。例如:
a := "hello, world!" b : = []byte (a)(3) 字符串尾部不包含 NULL 字符,這一點和 C/C++?不一樣。
(4) 字符串類型底層實現是一個二元的數據結構,一個是指針指向字節數組的起點,另一個是長度。 例如 :
type stringStruct struct {str unsafe.Pointer???//指向底層字節數組的指針len int??????????????//字節數組長度 }(5) 基于字符串創建的切片和原字符串指向相同的底層字符數組,一樣不能修改,對字符串的切片操作返回的子串仍然是 string,而非 slice。例如:
a := "hello , world!" b := a[0:4] c := a[1: ] d = a[ :4](6) 字符串和切片的轉換:字符串可以轉換為字節數組,也可以轉換為 Unicode 的字數組。例如:
a := "hello,世界!" b := [?]byte(a) c := [?]rune(a)(7) 字符串的運算。例如:
a := "hello" b := "world" c := a + b ? // 字符串的拼接 len(a) ? // 內置的 len 函數獲取字符串長度d := "hello,世界!" for i := 0; i < len(d);?i++ {fmt . Println(d[i]) }for i, v := range d {?????????fmt . Println(i , v) }?
rune 類型
Go 內置兩種字符類型:一種是 byte 的字節類類型( byte 是 uint 的別名),另一種是表示 Unicode 編碼的字符 rune。rune 在 Go 內部是 int32 類型的別名,占用 4 個字節。Go 語言默認的字符編碼就是 UTF-8 類型的,如果需要特殊的編碼轉換,則使用 Unicode/UTF-8 標準包。
package mainimport "fmt"func main() {asd := "123"var str []rune = []rune(asd)print(len(str))fmt.Printf("%c" , str[0]) }?
?
復合數據類型
顧名思義,復合數據類型就是由其他類型組合而成的類型。Go 語言基本的復合數據類型有指針、數組、切片、字典(map)、通道、結構和接口,它們的字面量格式如下:
* pointerType???? 指針類型使用*后面跟其指向的類型名 [n]?elementType?? 數組類型使用[n]后面跟數紐元素類型來表示,n 表示該數組的長度 []?elementType??? 切片類型使用[]后面跟切片元素類型來表示 map [keyType]valueType?? map 類型使用 map[鍵類型]值類型來表示chan valueType ?? 通道使用 chan 后面跟通道元素類型來表示struct { ?????????? 結構類型使用 struct{} 將各個結構字段擴起來表示feildType feildTypefeildType feildType... }interface { ?????? 接口類型使用 interface{}將各個方法括起來表示method1(inputParams) (returnParams)method2(inputParams) (returnParams)... }?
指針
Go 語言支持指針,指針的聲明類型為?*T,Go 同樣支持多級指針?**T。通過在變量名前加?&?來獲取變量的地址。指針的特點如下:
(1) 在賦值語句中,*T 出現在?"=" 左邊表示指針聲明,*T 出現在?"=" 右邊表示取指針指向的值( varName 為變量名)。示例如下:
var a = 11 p := &a ????*p 和 a 的位都是 11(2) 結構體指針訪問結構體字段仍然使用?"." 點操作符,Go 語言沒有?"->" 操作符。例如:
type User struct{name stringage int }andes := User{name:"andes",age:18, }p := &andes fmt.Println(p.name) ??? p.name 通過 "." 操作符訪問成員變量(3) Go 不支持指針的運算。
Go 由于支持垃圾回收,如果支持指針運算,則會給垃圾回收的實現帶來很多不便,在 C 和 C++?里面指針運算很容易出現問題,因此 Go 直接在語言層面禁止指針運算。例如:
a := 1234 p := &a ???? Go語言里面自增、自減操作符是語句而不是表達式 p++ ??????? 不允許,報 non-numeric type *int 錯誤(4) 函數中允許返回局部變量的地址,Go 編譯器使用"棧逃逸"?機制將這種局部變量的空間分配在堆上。例如:
func sum(a, b int) *int {sum := a + breturn &sum ?????? 允許,sum會分配在 heap 上 } package mainimport "fmt"func main() {var a int= 20 /* 聲明實際變量 */var ip *int /* 聲明指針變量 */ip = &a /* 指針變量的存儲地址 */fmt.Printf("a 變量的地址是: %x\n", &a )/* 指針變量的存儲地址 */fmt.Printf("ip 變量儲存的指針地址: %x\n", ip )/* 使用指針訪問值 */fmt.Printf("*ip 變量的值: %d\n", *ip ) }空指針判斷: if(ptr != nil) // ptr 不是空指針 if(ptr == nil) // ptr 是空指針 package mainimport "fmt"func main() {/* 定義局部變量 */var a int = 100var b int= 200fmt.Printf("交換前 a 的值 : %d\n", a )fmt.Printf("交換前 b 的值 : %d\n", b )/* 調用函數用于交換值* &a 指向 a 變量的地址* &b 指向 b 變量的地址*/swap(&a, &b);fmt.Printf("交換后 a 的值 : %d\n", a )fmt.Printf("交換后 b 的值 : %d\n", b ) }func swap(x *int, y *int) {var temp inttemp = *x /* 保存 x 地址的值 */*x = *y /* 將 y 賦值給 x */*y = temp /* 將 temp 賦值給 y */ } package main import "fmt"func main() {var a intvar ptr *intvar pptr **inta = 3000/* 指針 ptr 地址 */ptr = &a/* 指向指針 ptr 地址 */pptr = &ptr/* 獲取 pptr 的值 */fmt.Printf("變量 a = %d\n", a )fmt.Printf("指針變量 *ptr = %d\n", *ptr )fmt.Printf("指向指針的指針變量 **pptr = %d\n", **pptr) }?
?
數組
數組的類型名是?[n]elemetType,其中 n 是數組長度,elementType 是數組元素類型。 比如一個包含 2 個 int 類型元素的數組類型可表示為?[2]int。數組一般在創建時通過字面量初始化,單獨聲明一個數組類型變量而不進行初始化是沒有意義的。
數組初始化
a := [3]int{ 1, 2, 3}?? ?指定長度和初始化字面量 a := [ ... ]int{?1, 2, 3}? 不指定長度,但是由后面的初始化列表數量來確定其長度 a := [3]int{ 1:1, 2:3} ? 指定總長度,并通過索引值進行初始化,沒有初始化元素時使用類型默認值 a := [ ... ]int{?1:1, 2:3}??不指定總長度,通過索引值進行初始化,數組長度由最后一個索引值確定,沒有指定索引的元素被初始化為類型的零值數組的特點
(1) 數組創建完長度就固定了,不可以再追加元素。
(2) 數組是值類型的,數組賦值或作為函數參數都是值拷貝。
(3) 數組長度是數組類型的組成部分,[10]int 和 [20]int 表示不同的類型。
(4) 可以根據數組創建切片。
package mainimport "fmt"func main() {var n [10]int /* n 是一個長度為 10 的數組 */var i,j int/* 為數組 n 初始化元素 */ for i = 0; i < 10; i++ {n[i] = i + 100 /* 設置元素為 i + 100 */}/* 輸出每個數組元素的值 */for j = 0; j < 10; j++ {fmt.Printf("Element[%d] = %d\n", j, n[j] )} } package mainimport "fmt"func main() {/* 數組長度為 5 */var balance = [5]int {1000, 2, 3, 17, 50}var avg float32/* 數組作為參數傳遞給函數 */avg = getAverage( balance, 5 ) ;/* 輸出返回的平均值 */fmt.Printf( "平均值為: %f ", avg ); } func getAverage(arr [5]int, size int) float32 {var i,sum intvar avg float32 for i = 0; i < size;i++ {sum += arr[i]}avg = float32(sum) / float32(size)return avg; }?
?
切片(Slice)
Go 語言切片是對數組的抽象。Go 數組的長度不可改變,在特定場景中這樣的集合就不太適用,Go 中提供了一種靈活,功能強悍的內置類型切片("動態數組"),與數組相比切片的長度是不固定的,可以追加元素,在追加時可能使切片的容量增大。
type slice struct {array unsafe.Pointerlen intcap int )Go 為切片維護三個元素——指向底層數組的指針、切片的元素數量和底層數組的容量。具體結構如圖所示:
定義切片
聲明一個未指定大小的數組來定義切片(切片不需要說明長度):
var identifier []type或使用 make() 函數來創建切片:
var slice1 []type = make([]type, len) 也可以簡寫為 slice1 := make([]type, len)也可以指定容量,其中capacity為可選參數。 make([]T, length, capacity) 這里 len 是數組的長度并且也是切片的初始長度。?
切片初始化
s :=[] int {1,2,3 } 直接初始化切片,[]表示是切片類型,{1,2,3}初始化值依次是1,2,3。其cap = len = 3s := arr[:] 初始化切片s,是數組arr的引用s := arr[startIndex:endIndex] 將 arr 中從下標 startIndex 到 endIndex-1 下的元素創建為一個新的切片s := arr[startIndex:] 默認 startIndex 時將表示從 arr 的第一個元素開始s1 := s[startIndex:endIndex] 通過切片 s 初始化切片 s1s := make([]int,len,cap) 通過內置函數 make() 初始化切片s,[]int 標識為其元素類型為 int 的切片?
len() 和 cap() 函數
切片是可索引的,并且可以由 len() 方法獲取長度。切片提供了計算容量的方法 cap() 可以測量切片最長可以達到多少。以下為具體實例:
package mainimport "fmt"func main() {var numbers = make([]int,3,5)printSlice(numbers) }func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }?
空(nil)切片
一個切片在未初始化之前默認為 nil,長度為 0,實例如下:
package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)if(numbers == nil){fmt.Printf("切片是空的")} }func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }?
切片截取
可以通過設置下限及上限來設置截取切片?[lower-bound:upper-bound],實例如下:
package mainimport "fmt"func main() {/* 創建切片 */numbers := []int{0,1,2,3,4,5,6,7,8} printSlice(numbers)/* 打印原始切片 */fmt.Println("numbers ==", numbers)/* 打印子切片從索引1(包含) 到索引4(不包含)*/fmt.Println("numbers[1:4] ==", numbers[1:4])/* 默認下限為 0*/fmt.Println("numbers[:3] ==", numbers[:3])/* 默認上限為 len(s)*/fmt.Println("numbers[4:] ==", numbers[4:])numbers1 := make([]int,0,5)printSlice(numbers1)/* 打印子切片從索引 0(包含) 到索引 2(不包含) */number2 := numbers[:2]printSlice(number2)/* 打印子切片從索引 2(包含) 到索引 5(不包含) */number3 := numbers[2:5]printSlice(number3)}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }?
append() 和 copy() 函數
如果想增加切片的容量,我們必須創建一個新的更大的切片并把原分片的內容都拷貝過來。下面的代碼描述了從拷貝切片的 copy 方法和向切片追加新元素的 append 方法。
package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)/* 允許追加空切片 */numbers = append(numbers, 0)printSlice(numbers)/* 向切片添加一個元素 */numbers = append(numbers, 1)printSlice(numbers)/* 同時添加多個元素 */numbers = append(numbers, 2,3,4)printSlice(numbers)/* 創建切片 numbers1 是之前切片的兩倍容量*/numbers1 := make([]int, len(numbers), (cap(numbers))*2)/* 拷貝 numbers 的內容到 numbers1 */copy(numbers1,numbers)printSlice(numbers1) }func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }?
?
map
Go 語言內置的字典類型叫 map。map 的類型格式是:map[K]T,其中 K 可以是任意可以進行比較的類型,T 是值類型。map 也是一種引用類型。
(1) map 的創建
使用字面量創建。例如:
/* 聲明變量,默認 map 是 nil */ var map_variable map[key_data_type]value_data_typema := map[string]int{ "a": 1, "b": 2) fmt.Println(ma["a"] ) fmt.Println(ma["b"])使用內置的 make 函數創建。例如:
/* 使用 make 函數 */ map_variable := make(map[key_data_type]value_data_type)make(map[K]T ) //map 的容量使用默認位 make(map[K]T, len) //map 的容量使用給定的 len 值 mp1 := make(map[int]string) mp2 := make(map[int]string , 10) mp1[1] = "tom" mp2[1] =?"pony" fmt.Println(mp1[1]) //tom fmt.Println(mp2[1]) //pony?
(2) map 支持的操作
① map 的單個鍵值訪問格式為 mapName[key],更新某個 key 的值時 mapName[key]?放到等號左邊,訪問某個 key 的值時 mapName[key] 放在等號的右邊。
② 可以使用 range 遍歷一個 map 類型變量,但是不保證每次選代元素的順序。
③ 刪除 map 中的某個鍵值,使用如下語法:delete(mapName,key)。delete 是內置函數,用來刪除 map 中的某個鍵值對。
④ 可以使用內置的 len() 函數返回 map 中的鍵值對數量。例如:
mp := make(map[int]string) mp[1] = "tom" mp[1] = "pony" mp[2] = "jaky" mp[3] = "andes " delete (mp , 3)fmt.Println (mp [1]) fmt.Println(len(mp))? //?len函數返回 map 中的鍵值對的數量 for k, v := range mp { ? //?range 支持邊歷 mp,但不保證每次遍歷次序是一樣的fmt.Println("?key=", k,"value=", v) }注意:
(1) Go 內置的 map 不是并發安全的,并發安全的 map 可以使用標準包 sync?中的 map。
(2) 不要直接修改 map value 內某個元素的值,如果想修改 map 的某個鍵值,則必須整體賦值。例如:
type User struct {name stringage int }ma := make(map[int]User)andes := User{name : "andes",age: 18 , }ma[1]?= andes // ma [1].age = 19 // ERROR,不能通過 map 引用直接修改andes.age = 19 ma[1]?= andes ? // 必須整體替換 value fmt.Printf ("?%v\n ", ma) package mainimport "fmt"func main() {var countryCapitalMap map[string]string /*創建集合 */countryCapitalMap = make(map[string]string)/* map插入key - value對,各個國家對應的首都 */countryCapitalMap [ "France" ] = "巴黎"countryCapitalMap [ "Italy" ] = "羅馬"countryCapitalMap [ "Japan" ] = "東京"countryCapitalMap [ "India " ] = "新德里"/*使用鍵輸出map值 */for country := range countryCapitalMap {fmt.Println(country, "首都是", countryCapitalMap [country])}/*查看元素在集合中是否存在 */capital, ok := countryCapitalMap [ "American" ] /*如果確定是真實的,則存在,否則不存在 *//*fmt.Println(capital) *//*fmt.Println(ok) */if (ok) {fmt.Println("American 的首都是", capital)} else {fmt.Println("American 的首都不存在")} }輸出: France 首都是 巴黎 Italy 首都是 羅馬 Japan 首都是 東京 India 首都是 新德里 American 的首都不存在 package mainimport "fmt"func main() {/* 創建map */countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}fmt.Println("原始map")/* 打印map */for country := range countryCapitalMap {fmt.Println(country, "首都是", countryCapitalMap [ country ])}/*刪除元素*/ delete(countryCapitalMap, "France")fmt.Println("法國條目被刪除")fmt.Println("刪除元素后map")/*打印map*/for country := range countryCapitalMap {fmt.Println(country, "首都是", countryCapitalMap [ country ])} }輸出: 原始map India 首都是 New delhi France 首都是 Paris Italy 首都是 Rome Japan 首都是 Tokyo 法國條目被刪除 刪除元素后map Italy 首都是 Rome Japan 首都是 Tokyo India 首都是 New delhi?
?
范圍(Range)
Go 語言中 range 關鍵字用于 for 循環中迭代數組、切片、通道(channel)或集合(map)的元素。在數組和切片中它返回元素的索引和索引對應的值,在集合中返回 key-value 對。
package main import "fmt" func main() {//這是我們使用range去求一個slice的和。使用數組跟這個很類似nums := []int{2, 3, 4}sum := 0for _, num := range nums {sum += num}fmt.Println("sum:", sum)//在數組上使用range將傳入index和值兩個變量。上面那個例子我們不需要使用該元素的序號,所以我們使用空白符"_"省略了。有時侯我們確實需要知道它的索引。for i, num := range nums {if num == 3 {fmt.Println("index:", i)}}//range也可以用在map的鍵值對上。kvs := map[string]string{"a": "apple", "b": "banana"}for k, v := range kvs {fmt.Printf("%s -> %s\n", k, v)}//range也可以用來枚舉Unicode字符串。第一個參數是字符的索引,第二個是字符(Unicode的值)本身。for i, c := range "go" {fmt.Println(i, c)} }輸出: sum: 9 index: 1 a -> apple b -> banana 0 103 1 111?
?
struct
Go 中的 struct 類型和 C 類似,由多個不同類型元素組合而成。這里面有兩層含義:第一 ,struct 結構中的類型可以是任意類型;第二, struct 的存儲空間是連續的,其字段按照聲明時的順序存放(注意字段之間有對齊要求)。struct 有兩種形式:一種是 struct 類型字面量,另一種是使用 type 聲明的自定義 struct 類型。
(1)?struct 類型字面量的聲明格式:
struct {FeildName FeildTypeFeildName FeildTypeFeildName FeildType }(2) 自定義 struct 類型聲明格式:
type TypeName struct {FeildName FeildTypeFeildName FeildTypeFeildName FeildType }實際使用 struct 字面量的場景不多,更多的時候是通過 type 自定義一個新的類型來實現的。type 是自定義類型的關鍵字,不但支持 struct 類型的創建,還支持任意其他子定義類型的創建。
?
(3) struct 類型變量的初始化。示例如下:
type Person struct {Name stringAge int }type Student struct {*PersonNumber int }a := Person{"Tom", 21) 按照類型聲明順序,逐個賦值,一旦 struct 增加字段,則 整個初始化語句會報錯推薦下面這種使用 Feild 名字的初始化方式,沒有指定的字段則默認初始化為類型的零值 p := &Person{Name:"tata",Age: 12 , }s := Student{Person: p,Number: 110, } package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func main() {var Book1 Books /* Declare Book1 of type Book */var Book2 Books /* Declare Book2 of type Book *//* book 1 描述 */Book1.title = "Go 語言"Book1.author = "www.runoob.com"Book1.subject = "Go 語言教程"Book1.book_id = 6495407/* book 2 描述 */Book2.title = "Python 教程"Book2.author = "www.runoob.com"Book2.subject = "Python 語言教程"Book2.book_id = 6495700/* 打印 Book1 信息 */printBook(&Book1)/* 打印 Book2 信息 */printBook(&Book2) } func printBook( book *Books ) {fmt.Printf( "Book title : %s\n", book.title)fmt.Printf( "Book author : %s\n", book.author)fmt.Printf( "Book subject : %s\n", book.subject)fmt.Printf( "Book book_id : %d\n", book.book_id) }?
?
控制結構
現代計算機存儲結構無論 "普林斯頓結構",還是 "哈佛結構",程序指令都是線性地存放在存儲器上。程序執行從本質上來說就是兩種模式:順序和跳轉。
順序就是按照程序指令在存儲器上的存放順序逐條執行。 跳轉就是遇到跳轉指令就跳轉到某處繼續線性執行。Go 是一門高級語言,其源程序雖然經過了高度的抽象并封裝了很多語法糖,但還是跳不出這個模式(這里暫時不考慮 goroutine 引入并發后的執行視圖變化)。
順序在 Go 里面體現在從 main 函數開始逐條向下執行,就像我們的程序源代碼順序一樣;跳轉在 Go 里面體現為多個語法糖,包括 goto 語句和函數調用、分支( if、switch 、select )、循環( for )等。跳轉分為兩種:一種是無條件跳轉,比如函數調用和 goto 語句;一種是有條件的跳轉,比如分支和循環。
備注:
Go 的源代碼的順序并不一定是編譯后最終可執行程序的指令順序,這里面涉及語言的運行時和包的加載過程。上面論述的主要目的是使讀者從宏觀上整體理解程序的執行過程,建立一個從源代碼到執行體的大體映射概念,這個概念不那么精準,但對我們理解源程序到目標程序的構建非常有幫助。
?
if 語句特點
(1) if 后面的條件判斷子句不需要用小括號括起來。
(2) " { "?必須放在行尾,和 if?或 if else 放在一行。
(3) if 后面可以帶一個簡單的初始化語句,并以分號分割,該簡單語句聲明的變量的作用域是整個 if 語句塊,包括后面的?else if 和 else?分支。
(4) Go 語言沒有條件運算符 (a > b ??a:b),這也符合 Go 的設計哲學,只提供一種方法做事情。
(5) if 分支語句遇到 return 后直接返回,遇到 break 則跳過 break 下方的 if 語句塊。
if x <= y {return y } else {return x }if?x :=f(); x<y { ?// 初始化語句中的聲明變量 xreturn x } else if x > z { ?//?x 在 else if 里面一樣可以被訪問return z } else {return y } package main import "fmt"func main() {/* 局部變量定義 */var a int = 100;/* 判斷布爾表達式 */if a < 20 {/* 如果條件為 true 則執行以下語句 */fmt.Printf("a 小于 20\n" );} else {/* 如果條件為 false 則執行以下語句 */fmt.Printf("a 不小于 20\n" );}fmt.Printf("a 的值為 : %d\n", a); }?
switch 語句
switch 語句會根據傳入的參數檢測并執行符合條件的分支 。switch 的語法特點如下:
(1) switch 和 if?語句一樣,switch 后面可以帶一個可邊的簡單的初始化語句。
(2) switch 后面的表達式也是可選的,如果沒有表達式,則 case 子句是一個布爾表達式,而不是一個值,此時就相當于多重 if else 語句。
(3) switch 條件表達式的值不像 C 語言那樣必須限制為整數,可以是任意支持相等比較運算的類型變量。
(4) 通過 fallthough 語句來強制執行下一個 case?子句(不再判斷下一個 case 子句的條件是否滿足)。
(5) switch 支持 default 語句,當所有的 case 分支都不符合時,執行 default 語句, 并且 default?語句可以放到任意位置,并不影響 switch 的判斷邏輯。
(6) switch 和 .(type) 結合可以進行類型的查詢。
switch i :=?"?Y ";?i{???? //switch 后面可以帶上一個初始化語句case "y","?Y": ???? //多個 case 值使用逗號分隔fmt.Println ("yes"?) //yesfallthrough????????? //fallthrough 會跳過接下來的 case 條件表達式//直接執行下一個 case 語句case "n","N":fmt . Println("?no"?) ??//?no }switch { case score >= 90:grade = 'A' case score >= 80 :grade ='B' case score >= 70 :grade ='C' case score >= 60 :grade = 'D' default :grade ='F' } fmt.Printf("?grade?= %c\n ",?grade) ???//grade=B package mainimport "fmt"func main() {/* 定義局部變量 */var grade string = "B"var marks int = 90switch marks {case 90: grade = "A"case 80: grade = "B"case 50,60,70 : grade = "C"default: grade = "D" }switch {case grade == "A" :fmt.Printf("優秀!\n" ) case grade == "B", grade == "C" :fmt.Printf("良好\n" ) case grade == "D" :fmt.Printf("及格\n" ) case grade == "F":fmt.Printf("不及格\n" )default:fmt.Printf("差\n" );}fmt.Printf("你的等級是 %s\n", grade ); } package mainimport "fmt"func main() {switch {case false:fmt.Println("1、case 條件語句為 false")fallthroughcase true:fmt.Println("2、case 條件語句為 true")fallthroughcase false:fmt.Println("3、case 條件語句為 false")fallthroughcase true:fmt.Println("4、case 條件語句為 true")case false:fmt.Println("5、case 條件語句為 false")fallthroughdefault:fmt.Println("6、默認 case")} }輸出: 2、case 條件語句為 true 3、case 條件語句為 false 4、case 條件語句為 true?
for 語句
Go 語言僅支持一種循環語句,即 for 語句,同樣遵循 Go 的設計哲學,只提供一種方法做事情,把事情做好。Go 對應 C 循環的三種場景如下:
(1) 類似 C 里面的 for 循環語句
for init; condition; post { } init:一般為賦值表達式,給控制變量賦初值; condition:關系表達式或邏輯表達式,循環控制條件; post:一般為賦值表達式,給控制變量增量或減量。 package mainimport "fmt"func main() {sum := 0for i := 0; i <= 10; i++ {sum += i}fmt.Println(sum) }(2) 類似 C 里面的 while 循環語句
for condition { } package mainimport "fmt"func main() {sum := 1for ; sum <= 10; {sum += sum}fmt.Println(sum)// 這樣寫也可以,更像 While 語句形式for sum <= 10{sum += sum}fmt.Println(sum) }(3) 類似 C 里面的 while (1) 死循環語句
for { } package mainimport "fmt"func main() {sum := 0for {sum++ // 無限循環下去}fmt.Println(sum) // 無法輸出 }for 還有一種用法,是對數組、切片、字符串、map 和通道的訪問,語法格式如下:
訪問 map for key, value := range map{} for key := range map{}訪問數組 for index, value :=?range arry{} for index := range arry{} for , value :=?range arry{}訪問切片 for index, value :=?range slice{} for index := range slice{} for _, value :=?range slice{}訪問通道 for value :=?range channel {} package main import "fmt"func main() {strings := []string{"google", "runoob"}for i, s := range strings {fmt.Println(i, s)}numbers := [6]int{1, 2, 3, 5}for i,x:= range numbers {fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)} }輸出: 0 google 1 runoob 第 0 位 x 的值 = 1 第 1 位 x 的值 = 2 第 2 位 x 的值 = 3 第 3 位 x 的值 = 5 第 4 位 x 的值 = 0 第 5 位 x 的值 = 0?
?
標簽和跳轉
Go 語言使用標簽 ( Lable )來標識一個語句的位置,用于 goto、break、continue 語句的跳轉,標簽的語法是:Lable:Statement。
goto
goto 語句用于函數的內部的跳轉,需要配合標簽一起使用,具體的格式如下: goto Lable
goto Lable 的語義是跳轉到標簽名后的語句處執行,goto 語句有以下幾個特點:
(1) goto 語句只能在函數內跳轉。
(2) goto 語句不能跳過內部變量聲明語句,這些變量在 goto 語句的標簽語句處又是可見的。例如:
goto L ?//BAD,跳過 v := 3 這條語句是不允許的v := 3 L:(3) goto 語句只能跳到同級作用域或者上層作用域內,不能跳到內部作用域內。例如:
if n%2 == 1 {goto L1 } for n > 0?{f()n-- L1:f()n-- } package main import "fmt"func main() {//print9x()gotoTag() }//嵌套for循環打印九九乘法表 func print9x() {for m := 1; m < 10; m++ {for n := 1; n <= m; n++ {fmt.Printf("%dx%d=%d ",n,m,m*n)}fmt.Println("")} }//for循環配合goto打印九九乘法表 func gotoTag() {for m := 1; m < 10; m++ {n := 1LOOP: if n <= m {fmt.Printf("%dx%d=%d ",n,m,m*n)n++goto LOOP} else {fmt.Println("")}n++} }?
break
break 用于函數內跳出 for、switch、select 語句的執行,有兩種使用格式:
(1) 單獨使用,用于跳出 break 當前所在的 for、switch、select 語句的執行。
(2) 和標簽一起使用,用于跳出標簽所標識的 for、switch 、select 語句的執行。可用于跳出多重循環,但標簽和 break 必須在同一個函數內。例如:
L1:for i :=?0; ; i++{for j := 0; ; j ++?{if i >= 5 {// 跳出?L1 標簽所在的 for 循環break L1}if j > 10 {// 默認僅跳出離 break 最近的內層循環break}}} package mainimport "fmt"func main() {// 不使用標記fmt.Println("---- break ----")for i := 1; i <= 3; i++ {fmt.Printf("i: %d\n", i)for i2 := 11; i2 <= 13; i2++ {fmt.Printf("i2: %d\n", i2)break}}// 使用標記fmt.Println("---- break label ----")re:for i := 1; i <= 3; i++ {fmt.Printf("i: %d\n", i)for i2 := 11; i2 <= 13; i2++ {fmt.Printf("i2: %d\n", i2)break re}} }輸出: ---- break ---- i: 1 i2: 11 i: 2 i2: 11 i: 3 i2: 11 ---- break label ---- i: 1 i2: 11?
continue
continue 用于跳出 for 循環的本次選代,跳到 for 循環的下一次選代的 post 語句處執行,也有兩種使用格式:
(1) 單獨使用,用于跳出 continue 當前所在的 for 循環的本次迭代。
(2) 和標簽一起使用,用于跳出標簽所標識的 for 語句的本次選代,但標簽和 continue 必須在同一個函數內。例如:
L1:for i := 0; ; i++ {for j := 0 ; ; j ++?{if i >= 5 {// 跳到 Ll 標簽所在的 for 循環 i++ 處執行continue L1//?the following is not executed}if j > 10 {continue}}} package mainimport "fmt"func main() {// 不使用標記fmt.Println("---- continue ---- ")for i := 1; i <= 3; i++ {fmt.Printf("i: %d\n", i)for i2 := 11; i2 <= 13; i2++ {fmt.Printf("i2: %d\n", i2)continue}}// 使用標記fmt.Println("---- continue label ----")re:for i := 1; i <= 3; i++ {fmt.Printf("i: %d\n", i)for i2 := 11; i2 <= 13; i2++ {fmt.Printf("i2: %d\n", i2)continue re}} }輸出: ---- continue ---- i: 1 i2: 11 i2: 12 i2: 13 i: 2 i2: 11 i2: 12 i2: 13 i: 3 i2: 11 i2: 12 i2: 13 ---- continue label ---- i: 1 i2: 11 i: 2 i2: 11 i: 3 i2: 11?
總結
以上是生活随笔為你收集整理的Go 语言学习笔记(一):基础知识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux 环境下安装 Golang
- 下一篇: Go 语言学习笔记(二):函数