Go基礎知識總結
1. 變數
變數的宣告有四種方式:
-
宣告一個變數,預設的初始化值為0:
var a int
-
宣告一個變數,初始值為100:
var a int = 100
-
初始化時候省略資料型別,通過值自動推導變數的資料型別:
var a = 100
-
省略掉var關鍵字,直接自動匹配,但要使用
:=
a := 100
一個注意的點:第四種宣告變數的方式
a := 100
只能在區域性方法中使用,全域性變數不支援這種寫法
多個變數一起宣告的寫法:
-
單行寫法
var a, b int = 100, 200
var a, b = 100, "abc"
a, b := 100, "abc"
-
多行寫法
var ( a int = 100 b string = "abc" )
匿名變數
go中使用下劃線_
來作為匿名變數。
go支援函式多返回值,而當我們對於某個函式的返回值是不關心的時候,可以使用匿名變數來接收
比如:fd, _ := os.Open(xxx)
,對於第二個返回值我們並不想要,就可以直接用_
接收
2. 常量
go中常量使用關鍵字const
定義常量與定義變數方式類似,只是將關鍵字var
換成了const
,但常量定義沒有:=
這種寫法
比如:
const a int = 100
const (
a = 10
b = 20
)
3. iota
關鍵字
iota
用於與const
表示列舉型別
go中定義列舉使用的是iota
和const
,如下程式碼,定義一個列舉
const ( RED = iota BLUE BLACK .... )
注意:在
const
中新增一個關鍵字iota
,每一行的iota都會累加1,第一行的iota預設值是0
因此上面的,RED=0,BLUE=1,BLACK=2
但是如果第一行的RED我們賦值為5 * iota,那麼RED=5 * 0=0,BLUE=5 * 1=5,BLACK=5 * 2=10
因為每一行的iota
自動累加1,每一行相當於是5 * iota
因此有一個常見的例項,使用iota
來進行左移運算實現儲存單位的常量列舉:
const ( _ = iota // 賦值給_忽略這個值 B = 1 << (10 * iota) KB MB GB TB ... )
4.函式
go函式是允許有多個返回值的。go的函式定義可以有以下幾種寫法:
-
返回多個值,使用匿名變數
func test(a string, b int) (int, int) { .... return 100, 200 }
-
返回多個值,有引數名稱的
func test(a string, b int) (c, d int) { ... c = 100 d = 200 return }
注意:
-
c和d屬於test方法的形參,初始值預設為0,他們的作用空間也僅限於test方法,當已經給返回值變數賦值後,可以直接return就好了。
-
也可以返回別的變數, 比如內部在定義一個
e := 300
,最後return c, e
-
5. init函式
init函式是go在每個包初始化後自動執行的,而且在main函式之前執行
因此,init函式常用來:對變數初始化,註冊等。
init函式的幾個特點:
-
init函式用於包的初始化,是在
package xxxx
的時候完成的,在main
之前完成 -
每個包中是可以擁有多個init函式的,每個包的原始檔也是可以有多個init函式的
-
不同包的init函式是需要根據包匯入的依賴關係決定的(因為init是在package xxx之後完成)
所以是類似棧的結構,最後的包的init方法先執行
-
init函式不能被其他函式呼叫,也不需要傳入引數,也無返回值
package main
import "fmt"
func int() {
fmt.Println("init ok")
}
func main() {
fmt.Println("main...")
}
6. import 導包
go中使用import
進行導包操作,有幾種情況需要了解下:
-
import _ "fmt"
這種使用
_
的方式,是給fmt包起一個別名,是一個匿名,這樣子會無法使用包中的方法,但是一旦導包,就會執行包裡的init()
方法 -
import aa "fmt
"這種方式是給fmt包起一個別名aa,呼叫包中方法時候,就可以使用aa,比如
aa.Println()
-
import . "fmt"
這種方式是將fmt包中的所有方法全部匯入到當前包中,那麼fmt包中的所有方法都可以直接當成本包的方法來呼叫了,不用再加包名fmt(但這樣本包就不能定義與fmt包所有函式的函式名相同的函數了)
7. defer
defer關鍵字是go獨有的,是一種延遲語句,在函式return前執行defer。
一個函式中可以新增多個defer語句,執行順序是逆序的,先定義的defer最後執行
一般defer用於資源的關閉操作比較多。
有個文章可以看看Golang中defer、return、返回值之間執行順序的坑
結論就是:return最先執行,return負責將結果寫入返回值中;接著defer開始執行一些收尾工作;最後函式攜帶當前返回值退出。
8. 陣列
-
宣告陣列的方式
var myArray1 [10]int
myArray1 := [5]int{1,2,3,4}
-
陣列長度是固定的
-
固定長度的陣列在傳參的時候,是嚴格匹配陣列型別的
func add(array [4]int) { fmt.Println(array[0], array[1], array[2], array[3]) } func main() { arr := [5]int{1, 2, 3, 4} add(arr) }
這樣子傳參是不行的,報錯:
cannot use arr (variable of type [5]int) as type [4]int in argument to add
,引數是[4]int型別,傳參是[5]int -
需要注意的是,陣列是一個值型別,在賦值和作為引數傳遞時將產生一次複製動作。
9. 陣列切片(slice)
陣列切片slice,也叫動態陣列。
建立陣列切片有兩種方式:基於陣列和直接建立
-
基於陣列建立
func main() { // 先定義一個數組 var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10} // 基於陣列建立一個數組切片 var mySlice []int = myArray[:5] }
注意:go語言支援用
myArray[first:last]
這樣的方式基於陣列生成一個數組切片,這種[first,last]
是左閉右開的。如果基於myArray的所有元素建立陣列切片:
mySlice := myArray[:]
基於myArray的前5個元素建立陣列切片:
mySlice := myArray[:5]
基於myArray的第5個元素開始到所有元素建立切片:
mySlice := myArray[5:]
-
直接建立
使用Go提供的內建函式make(),比如:
-
建立一個初始元素個數為5的陣列切片,元素初始值為0:
mySlice := make([]int, 5)
-
建立一個初始元素個數為5的陣列切片,初始值為0,並預留10個元素的儲存空間:
mySlice := make([]int, 5, 10)
-
元素的遍歷
-
使用len()函式獲取元素個數
for i := 0; i < len(mySlice); i++ { .... }
-
使用
range
關鍵字遍歷for i, v := range mySlice { .... }// i 是index v是元素值
動態增減元素:
-
陣列切片支援內建函式
cap()
和len()
,cap()
函式返回的是陣列切片分配的空間大小,而len()
函式返回的是陣列切片中當前所儲存的元素個數。 -
如果需要新增元素,可以使用
append()
函式,生成一個新的陣列切片mySlice = append(mySlice, 1, 2, 3)
注意:
-
函式
append()
的第二個引數開始是一個不定引數,可以新增若干個元素 -
也可以將一個數組切片追加到另一個數組切片的末尾
mySlice2 := []int{8, 9, 10} mySlice = append(mySlice, mySlice2...)
這裡需要注意,第二個引數mySlice2後面加了三個點,也就是一個省略號,如果沒有這個省略號的話會編譯錯誤,因為append方法從第二個引數開始的所有引數都是待新增的元素,加上省略號相當於將mySlice2包含的元素逐個打散再加入
-
-
陣列切片擴容的機制
在append的時候,如果長度增加後超過容量,比如
mySlice := make([]int, 3, 4)
,切片mySlice的容量是4個,當前長度是3個元素,那麼在執行append,mySlice = append(mySlice, ,3, 4, 5)
後,新增3個元素,加上之前的元素就總共有6個了,超過了容量4,所以這時候切片需要擴容,而擴容的機制就是原始容量的2倍,也就是在新增元素後發現超過了原始的容量的話,會自動以初始容量的2倍去擴容 -
切片複製
使用內建函式
copy()
,用於將內容從一個數組切片複製到另一個數組切片。如果加入的兩個陣列切片沒有一樣大,就會按其中較小的那個陣列切片的元素個數進行復制。
slice1 := []int{1, 2, 3, 4, 5} slice2 := []int{6, 7, 8} copy(slice2, slice2) // 只會複製slice1的前三個元素到slice2中 // slice2 = {1,2,3} slice1 = {1,2,3,4,5} copy(slice1, slice2) // 只會複製slice2的3個元素到slice1的前3個位置 // slice2 = {6,7,8} slice1 = {6,7,8,4,5}
-
動態陣列在傳參上是引用傳遞的,而且不同元素長度的動態陣列他們的形參是一致的
func printArray(myArray []int) { ... }
10. map
-
map的宣告
var myMap map[int]string
其中myMap是變數名,int是鍵的型別,string是值的型別
只宣告沒有建立的map還不可用!
-
map的建立
使用make()函式建立:
myMap = make(map[int]string, 10)
10表示的是map的容量,與切片的容量類似
-
map的賦值
-
可以先宣告,再建立,最後賦值
var myMap map[int]string myMap = make(map[int]string, 10) myMap[0] = "java" myMap[1] = "Go"
-
直接使用
:=
myMap := make(map[int]string) myMap[0] = "java" myMap[1] = "Go"
-
宣告時賦值
myMap := map[int]string{ 0: "java", 1: "Go", }
-
-
元素刪除
使用內建函式
delete()
,用於刪除容器內的元素delete(myMap, 0)
,第二個引數是鍵,如果這個鍵不存在,啥也不會發生,也不會有影響。但如果傳入的map是nil,則會丟擲異常panic
-
元素查詢
從map中查詢一個特定的鍵,可以使用如下程式碼:
value, ok := myMap[1] if ok { // 找到了 .... }
只需要檢視第二個返回值ok是否為true就知道找沒找到,不需要像其他語言那樣檢查取到的值是不是為nil
11. 面向物件
我們都知道面向物件三個特點:封裝,繼承,多型。
但是go中並不像其他面嚮物件語言那樣有很多的概念,go語言的面向物件程式設計是基於語言型別系統的,整個型別系統通過介面串聯。
1. 型別系統
go語言中的型別是可以新增方法的,可以給任何型別,包括內建型別增加新方法。比如:
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
// 可以這樣使用
func main() {
var a Integer = 1
if a.Less(2) {
fmt.Println(a, "Less 2")
}
}
上面程式碼使用type
定義了一個新的型別Integer
,實質上它就是一個int型別,然後就給這個新型別增加了個新方法Less()。
新增方法這個語法可以以java類的概念來理解為:Integer就是一個類,而a就相當於類中的this,而Less是類裡的一個方法,當然a就可以呼叫到類裡的成員了,但是這裡的類實質是一個int,所以也就成員變數就是自身int值變量了,但如果a是一個結構體那就有成員變量了。
注意:當我們需要修改到物件的成員時,需要用到指標。比如程式碼修改為如下:
func (a *Integer) Add(b Integer) { *a += b }
這裡需要修改到物件a的值,所以需要用指標引用。
如果沒有需要修改物件的值,go並不要求一定要用指標的,有時候物件很小,用指標傳遞反而不划算
其實上面用指標和不用指標的具體原因,歸根結底就是:Go語言的型別是基於值傳遞的,要修改變數的值,就需要傳遞指標。
2. 結構體
結構體的定義很簡單,基本和C一樣:
type Person struct {
name string
age int
}
// 新增一個方法
func (p *Person) setAge(age int) {
p.age = age
}
func (p Person) getAge() {
return p.age
}
當然,結構體也是go的一種型別,也是可以新增方法的,按我的理解,其實結構體就相當於是面向物件的類,新增的方法就是成員方法,而本身的成員變數就是類中的成員變數。
結構體初始化:
結構體初始化有以下幾種實現:
p := new(Person)
p := &Person{}
p := &Person{"zhangsan", 18}
p := &Person{name: "zhangsan", age: 20}
Go語言中沒有建構函式這種概念,物件的建立通常做法是交給一個全域性的建立函式來完成,以NewXXX
命名,表示建構函式:
func NewPerson(name string, age int) *Person {
return &Person{name, age}
}
3. 封裝
回到面向物件三要素,封裝,其實結構體就已經是封裝的實現了。
這裡有個注意的點就是:
類名,屬性名,方法名,首字母大寫表示對外(也就是其他包)可以訪問,否則只能在本包內訪問
4. 繼承
go語言其實也是提供了繼承的,只是採用的是組合的寫法,比如以下例子:
// 定義父類
type Animal struct {
name string
age int
}
// 父類方法
func (a *Animal) Say() {
fmt.Println("animal say...")
}
// 定義子類繼承父類
type Dog struct {
Animal
weight int
}
func main() {
d := &Dog{}
d.Say()
d.name = "旺財"
fmt.Println(d)
}
輸出:
animal say...
&{{旺財 0} 0}
沒有初始化值的變數會預設為對應型別的零值。
5. 多型
在理解go語言的多型之前,得先了解go語言的介面型別。
先來了解下其他語言的介面,在java中,對於介面的實現是必須在實現類中宣告要實現的介面的,如果要實現一個介面,需要像下面程式碼這樣編寫程式碼:
// 定義一個介面類
public interface Person {
// 介面方法
public void say();
}
// 定義實現類,需要使用關鍵字implements顯式的說明實現哪一個介面
class Teacher implements Person {
public void say() {
system.out.println("Hello 我是老師")
}
}
而在go語言中,一個類只要實現了介面要求的所有函式,就可以說這個類實現了這個介面,當然go中介面使用的關鍵字還是interface
比如:
有一個File類,並且該類有四個方法,Read(),Write(),Seek(),Close()
type File struct {
// ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error
然後有以下一些介面:
type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
}
type IReader interface {
Read(buf []byte) (n int, err error)
}
type IWriter interface {
Write(buf []byte) (n int, err error)
}
type ICloser interface {
Close() error
}
程式碼中可以看出,File類並沒有明確表示從這些介面中繼承,甚至對於File類來說都不知道有這些介面的存在,但是在go裡,認為File類實現了這些介面。
因此可以這樣子進行賦值:
var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)
實質上,這樣子不就是多型麼!
介面的賦值:
go語言中介面賦值分為以下兩種情況:
-
將物件例項賦值給介面
這種情況要求物件例項實現了介面的所有方法,就比如上面的例子:
var file1 IFile = new(File)
-
將一個介面賦值給另一個介面
在go語言中,只要兩個介面有相同的方法列表(次序不要求),那麼它們就是等同的,可以相互賦值。
但介面的賦值也不要求必須等價,如果介面A的方法列表是介面B的方法列表的子集,那麼介面B可以賦值給介面A,而介面A無法賦值給介面B,因為介面B中並沒有介面A中的其他方法,如果賦值給介面A了,當介面A呼叫一個存在於介面A而介面B不存在的方法,那就找不到了
比如:
假設有一個Writer介面和ReadWriter介面,實體類還是上面的File類
type Writer interface { Write(buf []byte) (n int, err error) } type ReadWriter interface { Read(buf []byte) (n int, err error) Write(buf []byte) (n int, err error) }
可以將ReadWriter介面的例項賦值給Writer介面:
var file ReadWriter = new(File) // 介面ReadWriter 賦值給 介面Writer var file1 Writer = file // 這樣子是可以的,這樣file1是Writer介面的例項,只有一個Write方法可以呼叫是正常的
但是反過來就不行了:
var file Writer = new(File) // 介面ReadWriter 賦值給 介面Writer var file1 ReadWriter = file // 這樣子是不可以的,這樣file1是ReadWriter介面的例項,當file1呼叫read方法時候,並沒有這個方法,因為他實質是Writer介面型別
介面查詢
介面查詢可以檢查介面所指向的物件例項是否實現了某個介面,從而進行介面轉換,比如:
var file Writer = new(File)
if file1, ok := file.(ReadWriter); ok {
...
}
這裡是Writer介面所指向的物件例項是File類,是實現了ReadWriter的,所以這裡ok會為true,file1是ReadWriter介面的例項,所以相當於是從Writer介面轉為了ReadWriter介面了。
萬能型別
在Go語言中,有這麼一種空介面,原始碼裡是這樣的:type any = interface{}
,是一個空介面,根據之前對介面實現的理解,空接口裡沒有任何方法,那麼就可以認為所有的型別其實都是實現了這個介面的,因此這個interface{}
可以指向任何物件,稱為Any型別,也叫萬能型別。
var a interface{} = new(int)
var b interface{} = new(string)
var c interface{} = struct{X int}{1}
a = 10
b = "hello"
fmt.Println(a, b, c) // 輸出:10 hello {1}
任何物件例項都實現了interface{}
,就類似於Java中的Object類一樣,那我們就可以用interface{}
型別引用任意的資料型別了,像上面的程式碼那樣,這用在函式中傳參就很有用了!
型別查詢(型別斷言)
基於Go語言所有的物件例項都實現了空介面interface{}
這個前提,那我們便可以直接了當的詢問介面指向的物件例項的型別:xxx.(type)
func test(arg interface{}) {
switch arg.(type) {
case int:
fmt.Println("int type")
case string:
fmt.Println("string type")
default:
fmt.Println("unknown type")
}
}
func main() {
var v1 interface{} = "hello"
var v2 int = 100
v3 := struct{ X int }{1}
test(v1)
test(v2)
test(v3)
}
12. 學習資料
《Go語言程式設計》