go語言學習筆記
go語言學習筆記(初級)
最近一直在學習go語言,因此打算學習的時候能夠記錄
一下筆記。我這個人之前是從來沒有記錄筆記的習慣,
一直以來都是靠強大的記憶力去把一些要點記住。
讀書的時候因為一直都是有一個很安靜和很專心的環(huán)境,
因此很多事情都能記得很清楚,思考的很透徹。但是隨著
年紀不斷增加,也算是經(jīng)歷了很多的事情,加上工作有時會讓人
特別煩悶,很難把心好好靜下來去學習,去思考大自然的終極
奧秘,因此需要記錄一些東西,這些東西一方面可以作為一種自我激勵
的機制,另一方面,也算是自己成長的軌跡吧。
一. 順序編程
1. 變量
go語言變量定義的關鍵字是var。類型放在變量名后:
var v1 int
var v2 string
var v3 [10]int //數(shù)組
var v4 []int //切片
var v5 struct{ //結構體
f int
}
var v6 *int //指針
var v7 map[string]int //map
var v8 func(a int) int //函數(shù)
每一行不需要以分號作為結尾。 var
關鍵字還有一種是把多個變量的申明放在一起,
var(
v1 string
v2 int
)
2. 變量初始化
有人說,變量初始化有什么好提的,那么簡單。是的,但是這里面確實還是有一些值得注意的點。
var a int = 10 //完整定義
var a = 10 //自動推斷是int型
a := 10 //自動推斷是int型,申明并未該變量賦值
第三種初始化方式無疑是最簡單的。
但是要注意的是,這里面第三種方式是和特別的,比如
var a int
a := 10
等價于
var a int
var a int
a = 10
這時候就會報一個重復定義的錯誤。
3. 變量賦值
變量賦值和我們通常的語言基本是一致的,但是多了多重賦值功能。
i,j=j,i
這就直接實現(xiàn)了兩個變量的交換。
4. 匿名變量
go語言的函數(shù)是多返回值的,因此可能有些值并沒有被用到,這時我們就需要一個占位符去忽略這些返回值。
func GetName() (firstName, lastName, nickName string) {
return "May", "Chan", "Chibi Maruko"
}
_, _, nickName := GetName()
5. 定義常量
通過const關鍵字,可以用來定義常量。
const Pi float64 = 3.1415926
const zero = 0.0 //自動推斷類型
const ( //多定義
size int64 = 10
hello = -1
)
const u , v float32 = 0.0 , 3.0 //多重賦值
const a , b , c = 1 , 2 , “hello” //自動推斷類型
常量的定義也可以跟一個表達式, 但是這個表達式應該是編譯的時候就可以求值的.
const mask = 1 << 3 //正常
const Home = os.GetEnv("HOME") //錯誤,運行時才能確定
6. 預定義常量
這里要講一個很有意思的東西, 叫做iota.
這個東西每一個const出現(xiàn)的位置被置為0,沒出現(xiàn)一個iota出現(xiàn),都自增1,到寫一個const出現(xiàn)的時候,又置為0.
const (
c1 = iota //0
c2 = iota //1
c3 = iota //2
)
const x = iota // x == 0 (因為iota又被重設為0了)
const y = iota // y == 0 (同上)
如果兩個const賦值表達式是一樣的,可以省略后面的賦值表達式.
const (
c1 = iota //0
c2 //1
c3 //3
)
const (
a = 1 <<iota // a == 1 (iota在每個const開頭被重設為0)
b // b == 2
c // c == 4
)
6. 枚舉
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberOfDays // 這個常量沒有導出
)
大寫字母開頭的包外可見, 小寫字母開頭的包外不可見.
7. 類型
-
整型
int8, uint8, int16, uint16,int32, uint32, int64, uint64, int, uint, uintptr
不同類型不能相互比較.
-
浮點類型
float32, float64
涉及的一些運算比較簡單, 我們不做細講.
-
字符串類型
下面我們展示一個完整的go語言程序, 也是以hello
world為主題, 畢竟hello world是一個萬斤油的主題.package main import "fmt" //引入依賴包 func main() { fmt.Println("hello,world!") }這基本上是一個最簡單的程序了,但是對于我們的學習非常有用,用這個模板可以寫出非常好的東西出來.
字符串串本身非常簡單,主要就是一些字符串操作, 比如取特定位置的字符等.
package main import "fmt" //引入依賴包 func main() { var str string = "hello,world!" fmt.Println(str) ch := str[0] //取某個特定位置的字符 fmt.Printf("%c\n",ch) length := len(str) fmt.Println(length) //len用來獲取長度 str = "你好,世界" ch = str[0] fmt.Printf("%c\n",ch) length = len(str) fmt.Println(length) }輸出結果為:
hello,world! h 12 ? 13這正好說明[]和len都不能處理中文.
字符串連接也是用+.
字符串的遍歷:
package main import "fmt" //引入依賴包 func main() { var str string = "hello,world!" n := len(str) for i := 0; i < n; i++ { ch := str[i] fmt.Printf("%c\n",ch) } }輸出結果:
h e l l o , w o r l d !對于中文, 結果是亂碼, 因為是一個字節(jié)一個字節(jié)輸出的, 但是默認是UTF8編碼, 一個中文對應3個字節(jié).
這里我們要提到一個range的關鍵字, 它可以把字符串按鍵值對的方式返回.package main import "fmt" //引入依賴包 func main() { var str string = "hello,world! 你好,世界!" for _, ch := range str { fmt.Printf("%c\n",ch) } }輸出結果為:
h e l l o , w o r l d ! 你 好 , 世 界 !事實上, 字符類型有兩種, 一種就是byte(uint8), 另一種是rune. 第一種遍歷字符串ch是byte, 而第二種是rune.
-
數(shù)組
數(shù)組這種類型是非常常見的
[32]byte [2*N] struct { x, y int32 } [1000]*float64 [3][5]int [2][2][2]float64數(shù)組的遍歷和字符串一樣,這里不再重復.
數(shù)組是值類型,在賦值時會拷貝一份.
-
數(shù)組切片
數(shù)組切片的概念比較復雜, 它有點類似于c++中vector的概念, 但又不完全一樣.
我們這里詳細提幾點.
-
切片的創(chuàng)建
切片有兩種創(chuàng)建方式, 一種是基于數(shù)組創(chuàng)建, 另一種是用make創(chuàng)建.
package main import "fmt" //引入依賴包 func main() { //從數(shù)組創(chuàng)建 var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10} var sa []int = myArray[5:] for _, e := range sa { fmt.Println(e) } fmt.Println(len(sa)) fmt.Println(cap(sa)) //從make創(chuàng)建 var mySlice2 []int = make([]int, 5, 10) for _, e := range mySlice2 { fmt.Println(e) } fmt.Println(len(mySlice2)) fmt.Println(cap(mySlice2)) //賦值 var mySlice3 []int = []int{1,2,3} for _, e := range mySlice2 { fmt.Println(e) } }slice是引用類型.
package main import "fmt" //引入依賴包 func test(a [10]int) { a[0] = 10 } func printArray(a [10]int){ for _, e := range a { fmt.Println(e) } } func main() { var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10} printArray(myArray) test(myArray) printArray(myArray) }輸出結果:
1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10我們發(fā)現(xiàn)數(shù)組確實是按照值來傳遞. 那么如果是slice呢, 會發(fā)生什么?
package main import "fmt" //引入依賴包 func test(a []int) { a[0] = 10 } func printArray(a []int){ for _, e := range a { fmt.Println(e) } } func main() { var myArray []int = []int{1,2,3,4,5,6,7,8,9,10} printArray(myArray) test(myArray) printArray(myArray) }輸出結果:
1 2 3 4 5 6 7 8 9 10 10 2 3 4 5 6 7 8 9 10確實是按照引用來傳遞的.
append函數(shù)可以往切片尾部增加元素.
mySlice = append(mySlice, 1, 2, 3)mySlice = append(mySlice, mySlice2...)...表示把一個slice拆成元素來處理.
package main import "fmt" //引入依賴包 func main() { var slice1 []int = make([]int,5,10) var slice2 []int = []int{1,2,3} fmt.Println(slice1) fmt.Printf("%p\n",slice1) slice1 = append(slice1,slice2...) fmt.Println(slice1) fmt.Printf("%p\n",slice1) slice1 = append(slice1,slice2...) fmt.Println(slice1) fmt.Printf("%p\n",slice1) }輸出結果:
[0 0 0 0 0] 0xc820012190 [0 0 0 0 0 1 2 3] 0xc820012190 [0 0 0 0 0 1 2 3 1 2 3] 0xc82005e000在這里我們看到,slice的地址是所隨著內(nèi)內(nèi)存的改變而變化的,因此是需要仔細思考的.我個人不覺得
go語言這種特性有什么好的,反正也是奇葩極了. 不過slice還提供copy, 也算是一些彌補吧.
-
-
map
go語言中,map使用非常簡單.基本上看代碼就會了.
package main import "fmt" //引入依賴包 //定義一個Person的結構體 type Person struct{ name string age int } func main() { var dic map[string]Person = make(map[string]Person , 100) //初始化map dic["1234"] = Person{name:"lilei",age:100} dic["12345"] = Person{name:"hanmeimei",age:20} dic["123456"] = Person{name:"dagong",age:30} fmt.Println(dic) //刪除dagong delete(dic,"123456") fmt.Println(dic) //查找某個key value,ok := dic["123456"] if ok { fmt.Println(value) } value,ok = dic["1234"] if ok { fmt.Println(value) } for k,v := range dic { fmt.Println(k + ":" + v.name) } }輸出結果為:
map[12345:{hanmeimei 20} 123456:{dagong 30} 1234:{lilei 100}] map[1234:{lilei 100} 12345:{hanmeimei 20}] {lilei 100} 12345:hanmeimei 1234:lileimap很簡單吧. 數(shù)據(jù)結構我們講完了, 接下來可以看看代碼的程序控制了.
8. 程序控制
程序控制本人只提一些關鍵性的東西,不會啰嗦太多.
-
switch語句
switch語句不需要在每個case地下寫break,默認就是執(zhí)行break.如果要執(zhí)行多個case, 在case最后加入fallthrough.
條件表達式不限制為常量或者整數(shù).單個case自然可以有多個結果可以選.package main import "fmt" //引入依賴包 func test(a int) { switch { case a < 0: fmt.Println("hello") case a == 10: fallthrough case a > 10 && a < 100: fmt.Println("world") default: fmt.Println("nima") } } func main() { test(-1) test(10) test(100) } -
循環(huán)
go語言的循環(huán)比較特別, 它用一個for就把for和while的活都干了.
package main import "fmt" //引入依賴包 func main() { sum := 0 for i := 0; i <= 100; i++ { sum += i } fmt.Println(sum) sum = 0 i := 0 for(i <= 100){ sum += i i++ } fmt.Println(sum) }break還支持break到指定的label處.
for j := 0; j < 5; j++ { for i := 0; i < 10; i++ { if i > 5 { break JLoop } fmt.Println(i) } } JLoop: // ... -
函數(shù)
函數(shù)是一個非常重要的概念, 也很簡單. go的函數(shù)以func關鍵字定義, 支持不定參數(shù)和多返回值. 函數(shù)名首字符的大小寫是很有講究的:
小寫字母開頭的函數(shù)只在本包內(nèi)可見,大寫字母開頭的函數(shù)才能被其他包使用。這個規(guī)則也適用于類型和變量的可見性。
package main import "fmt" //引入依賴包 //...int是不定參數(shù),實際上就是一個slice, a,b是多返回值 func SumAndAverage(sample ...int) (a , b float64) { a , b = 0 , 0 for _, d := range sample { a += float64(d) } if len(sample) == 0 { b = 0 }else{ b = a / float64(len(sample)) } return a , b } func main() { a , b := SumAndAverage(1, 2 , 3) fmt.Println(a , b) }很簡單吧. 注意, 如果是函數(shù)里面調(diào)了其他函數(shù), 那么這個sample怎么傳給其他喊函數(shù)呢?
sample... //...表示是拆成一個個元素傳遞匿名函數(shù)的概念也很簡單, 只要看代碼就會明白.
package main import "fmt" //引入依賴包 func main() { var myFunc func(...int)(float64, float64)= func(sample ...int) (a , b float64) { a , b = 0 , 0 for _, d := range sample { a += float64(d) } if len(sample) == 0 { b = 0 }else{ b = a / float64(len(sample)) } return a , b } a , b := myFunc(1, 2 , 3) fmt.Println(a , b) }下面是關于閉包的概念. 這個概念在許式偉的書中被詮釋的非常好:
閉包是可以包含自由(未綁定到特定對象)變量的代碼塊,這些變量不在這個代碼塊內(nèi)或者任何全局上下文中定義,
而是在定義代碼塊的環(huán)境中定義。
要執(zhí)行的代碼塊(由于自由變量包含在代碼塊中,所以這些自由變量以及它們引用的對象沒有被釋放)為自由變量提供綁定定的計算環(huán)境(作用域)。
閉包的實現(xiàn)確保只要閉包還被使用,那么被閉包引用的變量會一直存在.*我們來看來兩個閉包的例子.
package main import "fmt" //引入依賴包 func test(i int) func() { return func(){ fmt.Println(10+i) fmt.Printf("%p\n",&i) } } func main() { a := test(1); b := test(2) a() b() }輸出結果:
11 0xc82000a288 12 0xc82000a2c0我們從這個結果中發(fā)現(xiàn), i的地址是會變的, 因為是作為一個局部變量傳進去的.
package main import "fmt" //引入依賴包 func test(x int) func(int) int { return func(y int) int { fmt.Printf("%p\n",&x) return x + y } } func main() { a := test(1); fmt.Println(a(10)) fmt.Println(a(20)) }輸出結果:
0xc82000a288 11 0xc82000a288 21因為x只傳入了一次, 因此沒有改變.
package main import ( "fmt" ) func main() { var j int = 5 a := func() (func()) { var i int = 10 return func() { fmt.Printf("i, j: %d, %d\n", i, j) } }() a() j *= 2 a() }此時輸出:
i, j: 10, 5 i, j: 10, 10
b = a b.Mofify()
如果b改變, a不發(fā)生改變, 就是值語義. 如果b改變, a也發(fā)生改變, 就是引用語義.
go語言大多數(shù)類型都是值語義, 比如:
基本類型: byte, int, float32, float64, string
復合類型: struct, array, pointer
也有引用語義的, 比如:
slice, map, channel, interface.
這是我們要牢記的.
我們的筆記整體式按照許式偉的書來安排, 但是由于許的書提綱性很強, 內(nèi)容上不是很詳細, 基本上會重新整理補充一些東西進去.
-
結構體
結構體是用struct來申明的, 不多廢話, 直接上代碼.
package main
import (
"fmt"
)
//申明一個結構體
type Person struct{
Name string
Age int
}
func main() {
//結構體的初始化方式
//1. 直接賦值
var p Person
p.Name = "dingding"
p.Age = 10
fmt.Println(p)
//2.順序賦值
p1 := Person{"dingding",10}
fmt.Println(p1)
//3. key value 賦值
p2 := Person{Name:"dingding",Age:10}
fmt.Println(p2)
//4.指針賦值
p3 := &Person{Name:"dingding",Age:10}
fmt.Println(p3)
p4 := new(Person)
fmt.Println(p4)
fmt.Println("---------------------------")
a := p
a.Name = "dongdong"
b := p3
b.Name = "dongdong"
fmt.Println(p)
fmt.Println(p3)
}
輸出結果:
{dingding 10}
{dingding 10}
{dingding 10}
&{dingding 10}
&{ 0}
---------------------------
{dingding 10}
&{dongdong 10}
這說明,struct確實是值語義.
下面討論一下結構體的組合問題. 這點許的書中并沒有過多涉及, 但是還是很有必要的, 因為在實際場合中用的會很多.
package main
import (
"fmt"
)
//申明一個結構體
type Human struct{
Name string
Age int
Phone string
}
//再申明一個結構體
type Employee struct {
Person Human // 匿名字段Human
Speciality string
Phone string // 雇員的phone字段
}
func main() {
e := Employee{
Person:Human{
Name:"dingding",
Age:11,
Phone:"6666666",
},
Speciality:"aaa",
Phone:"77777777",
}
fmt.Println(e.Phone)
}
這段代碼看上去非常ok, 但是如果我們稍微改一下代碼呢? 比如把
Person改成匿名結構, 會有些好玩的現(xiàn)象.
package main
import (
"fmt"
)
//申明一個結構體
type Human struct{
Name string
Age int
Phone string
}
//再申明一個結構體
type Employee struct {
Human // 匿名字段Human
Speciality string
//Phone string // 雇員的phone字段
}
func main() {
e := Employee{
Human{"dingding",11,"6666666"},
"aaa",
//Phone:"77777777",
}
fmt.Println(e.Phone)
}
此時輸出的是6666666, 因為相當于它把Human的字段Phone繼承下來了.
如果Employee里也定義Phone字段呢?
package main
import (
"fmt"
)
//申明一個結構體
type Human struct{
Name string
Age int
Phone string
}
//再申明一個結構體
type Employee struct {
Human // 匿名字段Human
Speciality string
Phone string // 雇員的phone字段
}
func main() {
e := Employee{
Human{"dingding",11,"6666666"},
"aaa",
"77777777",
}
fmt.Println(e.Phone)
}
此時輸出的時77777777, 因為相當于是覆蓋. 那么怎么輸出6666666呢?
package main
import (
"fmt"
)
//申明一個結構體
type Human struct{
Name string
Age int
Phone string
}
//再申明一個結構體
type Employee struct {
Human // 匿名字段Human
Speciality string
Phone string // 雇員的phone字段
}
func main() {
e := Employee{
Human{"dingding",11,"6666666"},
"aaa",
"77777777",
}
fmt.Println(e.Phone)
fmt.Println(e.Human.Phone)
}
輸出結果:
77777777
6666666
所以, 匿名結構體的組合相當于有繼承的功能.
-
為類型添加方法
這個概念和java或者是C++非常不一樣, 它的理念是把似乎是把方法綁定到特定類型上去.
這個概念已經(jīng)不僅僅是對象的概念了, 事實上,
我也不知道google這幫人腦子是怎么想的, 這種搓劣的復古風格,
也是讓我打開眼界, 我個人覺得, go雖然仗著google的名氣, 似乎顯得很厲害, 但是,
比起java和C++, 簡直就是個玩具, 說的不好聽一點,
比起scala這樣的一出生就是個大胖子, go更像是個缺胳膊少腿的畸形兒.
好了, 不說了, 直接上代碼.
package main
import (
"fmt"
)
//go綁定方法必須是本包內(nèi)的,int不是main包內(nèi)定義的.
//因此需要type一下, Integer就是本包內(nèi)定義的類型
type Integer int
//為int綁定一個Print方法
func (i Integer) Println() {
fmt.Println(i)
}
func main() {
var a Integer = 10
a.Println()
}
結果輸出10, 如果是如下呢?
package main
import (
"fmt"
)
//go綁定方法必須是本包內(nèi)的,int不是main包內(nèi)定義的.
//因此需要type一下, Integer就是本包內(nèi)定義的類型
type Integer int
//為int綁定一個Print方法
func (i Integer) Println() {
fmt.Println(i)
}
func main() {
a := 10
a.Println()
}
輸出結果:
# command-line-arguments
./main.go:18: a.Println undefined (type int has no field or method Println)
因為a := 10, go會把a推斷為int, 但int并沒綁上Println方法.
注意:
//為int綁定一個Print方法
func (i Integer) Println() {
fmt.Println(i)
}
這里的i并不是引用類型,因此對i的修改不會反應到a上去.
package main
import (
"fmt"
)
//go綁定方法必須是本包內(nèi)的,int不是main包內(nèi)定義的.
//因此需要type一下, Integer就是本包內(nèi)定義的類型
type Integer int
//為int綁定一個Print方法
func (i Integer) Add() {
i += 2
}
func main() {
var a Integer = 10
a.Add()
fmt.Println(a)
}
輸出10.
如果我們用引用呢?
package main
import (
"fmt"
)
//go綁定方法必須是本包內(nèi)的,int不是main包內(nèi)定義的.
//因此需要type一下, Integer就是本包內(nèi)定義的類型
type Integer int
//為int綁定一個Print方法
func (this *Integer) Add() {
*this += 2
}
func main() {
var a Integer = 10
a.Add()
fmt.Println(a)
}
這時輸出12. 我們發(fā)現(xiàn), 這個this就像是我們C++里面的this指針一樣.
不過也傻13復古的多.
我們在看一個例子,
package main
import (
"fmt"
)
//go綁定方法必須是本包內(nèi)的,int不是main包內(nèi)定義的.
//因此需要type一下, Integer就是本包內(nèi)定義的類型
type Integer int
//為int綁定一個Print方法
func (this *Integer) Add() {
fmt.Println(this)
*this += 2
}
func main() {
var b Integer = 10
var a *Integer = &b
a.Add()
fmt.Println(a)
}
輸出結果:
0xc82000a288
0xc82000a288
我們發(fā)現(xiàn),
//為int綁定一個Print方法
func (this *Integer) Add() {
fmt.Println(this)
*this += 2
}
該方法用指針調(diào)用和用值去調(diào)用, 效果是一樣的. this都是指向原來對象的指針而已.
下面這個例子來自于許的書中,
package main
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
func main() {
var a Integer = 1
var b LessAdder = a
}
輸出:
# command-line-arguments
./main.go:20: cannot use a (type Integer) as type LessAdder in assignment:
Integer does not implement LessAdder (Add method has pointer receiver)
這個例子似乎有點奇怪, 為什么呢?
package main
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
type Less interface {
Less(b Integer) bool
}
type Adder interface {
Add(b Integer)
}
func main() {
var a Integer = 1
var b Adder = a
b.Add(10)
}
我們可以看得更清楚:
./main.go:28: cannot use a (type Integer) as type Adder in assignment:
Integer does not implement Adder (Add method has pointer receiver)
但如果是:
package main
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
type Less interface {
Less(b Integer) bool
}
type Adder interface {
Add(b Integer)
}
func main() {
var a Integer = 1
var b Integer = a
b.Add(10)
}
就沒有任何問題. 對比起來, 就是這兩行代碼:
var b Integer = a
var b Adder = a
我們接下去會娓娓道來其中的奧妙.
package main
import(
"fmt"
)
//定義對象People、Teacher和Student
type People struct {
Name string
}
type Teacher struct{
People
Department string
}
type Student struct{
People
School string
}
//對象方法實現(xiàn)
func (p *People) SayHi() {
fmt.Printf("Hi, I'm %s. Nice to meet you!\n",p.Name)
}
func (t *Teacher) SayHi(){
fmt.Printf("Hi, my name is %s. I'm working in %s .\n", t.Name, t.Department)
}
func (s *Student) SayHi() {
fmt.Printf("Hi, my name is %s. I'm studying in %s.\n", s.Name, s.School)
}
func (s *Student) Study() {
fmt.Printf("I'm learning Golang in %s.\n", s.School)
}
//定義接口Speaker和Learner
type Speaker interface{
SayHi()
}
type Learner interface{
SayHi()
Study()
}
func main() {
//初始化對象
people := People{"張三"}
// teacher := &Teacher{People{"鄭智"}, "Computer Science"}
// student := &Student{People{"李明"}, "Yale University"}
var speaker Speaker //定義Speaker接口類型的變量
speaker = people
speaker.SayHi()
}
這時就會出現(xiàn)上面我們提到的錯誤. 盡管如果我們這么去調(diào)用:
package main
import(
"fmt"
)
//定義對象People、Teacher和Student
type People struct {
Name string
}
type Teacher struct{
People
Department string
}
type Student struct{
People
School string
}
//對象方法實現(xiàn)
func (p *People) SayHi() {
fmt.Printf("Hi, I'm %s. Nice to meet you!\n",p.Name)
}
func (t *Teacher) SayHi(){
fmt.Printf("Hi, my name is %s. I'm working in %s .\n", t.Name, t.Department)
}
func (s *Student) SayHi() {
fmt.Printf("Hi, my name is %s. I'm studying in %s.\n", s.Name, s.School)
}
func (s *Student) Study() {
fmt.Printf("I'm learning Golang in %s.\n", s.School)
}
//定義接口Speaker和Learner
type Speaker interface{
SayHi()
}
type Learner interface{
SayHi()
Study()
}
func main() {
//初始化對象
people := People{"張三"}
// teacher := &Teacher{People{"鄭智"}, "Computer Science"}
// student := &Student{People{"李明"}, "Yale University"}
//var speacker Speaker //定義Speaker接口類型的變量
//speacker = people
people.SayHi()
}
或者
package main
import(
"fmt"
)
//定義對象People、Teacher和Student
type People struct {
Name string
}
type Teacher struct{
People
Department string
}
type Student struct{
People
School string
}
//對象方法實現(xiàn)
func (p *People) SayHi() {
fmt.Printf("Hi, I'm %s. Nice to meet you!\n",p.Name)
}
func (t *Teacher) SayHi(){
fmt.Printf("Hi, my name is %s. I'm working in %s .\n", t.Name, t.Department)
}
func (s *Student) SayHi() {
fmt.Printf("Hi, my name is %s. I'm studying in %s.\n", s.Name, s.School)
}
func (s *Student) Study() {
fmt.Printf("I'm learning Golang in %s.\n", s.School)
}
//定義接口Speaker和Learner
type Speaker interface{
SayHi()
}
type Learner interface{
SayHi()
Study()
}
func main() {
//初始化對象
people := People{"張三"}
// teacher := &Teacher{People{"鄭智"}, "Computer Science"}
// student := &Student{People{"李明"}, "Yale University"}
var speacker Speaker //定義Speaker接口類型的變量
speacker = &people
speacker.SayHi()
}
這樣就都沒有任何問題, 這就是說什么呢? 這說明對于對象的方法, 無論接受者是對象還是對象指針, 都沒
任何問題. 但是如果是借口,如果接口中存在某個方法,綁定的接收者是對象指針,那么這個接口
也只能被該對象指針賦值. 如此奇葩的設計, 我只能說, go的設計者真是個腦殘.
-
繼承
好了, 下面正式講繼承的語法, 話說那玩意兒的真的算不上繼承, 比C++的繼承真的時不知道low到哪里去了. 但是我也不知道為啥這是go愛好者們最愛標榜的東西,
有時我想想, 程序員也真是單純的人, 一點點的蠱惑, 就會讓他們激動不已,
感覺就要去參加革命了似的.
go的繼承非常簡陋, 就是一個匿名結構組合的問題. 不廢話,直接上代碼.
package main
import(
"fmt"
)
//基類
type Base struct{
Name string
}
//綁定Say方法
func (b *Base) Say() {
fmt.Println(b.Name)
}
//綁定ok方法
func (b *Base) Ok() {
fmt.Println("ok")
}
//Foo有個匿名結構Base
type Foo struct{
Base
Name string
}
//重寫Say方法
func (f *Foo) Say() {
f.Base.Say()
fmt.Println(f.Name)
}
func main() {
var f *Foo = &Foo{Base{"father"},"sun"}
f.Ok()
f.Say()
}
輸出結果:
ok
father
sun
ok,下面我們看看多繼承二義性的問題.
package main
import(
"fmt"
)
//father
type Father struct{
}
func (f *Father)Say() {
fmt.Println("father")
}
//mother
type Mother struct{
}
func (f *Mother)Say() {
fmt.Println("mother")
}
//sun
type Sun struct{
Father
Mother
}
func main() {
var s *Sun = new(Sun)
s.Say()
}
輸出:
# command-line-arguments
./main.go:32: ambiguous selector s.Say
果然展現(xiàn)了二義性. 消歧義方式也是土的掉渣:
package main
import(
"fmt"
)
//father
type Father struct{
}
func (f *Father)Say() {
fmt.Println("father")
}
//mother
type Mother struct{
}
func (f *Mother)Say() {
fmt.Println("mother")
}
//sun
type Sun struct{
Father
Mother
}
func main() {
var s *Sun = new(Sun)
s.Father.Say()
s.Mother.Say()
}
我也是醉了.
此外, 我們也會看到還有一種用法,
package main
import(
"fmt"
)
//基類
type Base struct{
Name string
}
//綁定Say方法
func (b *Base) Say() {
fmt.Println(b.Name)
}
//綁定ok方法
func (b *Base) Ok() {
fmt.Println("ok")
}
//Foo有個匿名結構Base
type Foo struct{
*Base //base是個指針
Name string
}
//重寫Say方法
func (f *Foo) Say() {
f.Base.Say()
fmt.Println(f.Name)
}
func main() {
var f *Foo = &Foo{&Base{"father"},"sun"}
f.Ok()
f.Say()
}
這里Foo的Base是一個指針類型, 因此我們傳入的也必須是個指針, 我們可以看到這種用法.
-
接口
這個我不想吐槽什么, 前面已經(jīng)吐槽過了, 但是這種設計, 確實也是詭異之極.
go的借口是非侵入式的, 只要實現(xiàn)了接口定義的方法, 就等于實現(xiàn)了該接口. 換句話說, 接口的實現(xiàn)和定義式可以分離
不相關的.
接口的例子還是之前那個:
package main
import(
"fmt"
)
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
//定義接口
type LessAdder interface {
Less(b Integer) bool //函數(shù)聲明
Add(b Integer) //函數(shù)聲明
}
func main() {
var a Integer = 10
var b LessAdder = &a //道理我們前面提到過了,Add接收者是個對象指針
fmt.Println(b.Less(5))
b.Add(20)
fmt.Println(a)
}
輸出:
false
30
只要兩個接口擁
有相同的方法列表(次序不同不要緊),那么它們就是等同的,可以相互賦值。
接口賦值并不要求兩個接口必須等價。如果接口A的方法列表是接口B的方法列表的子集,
那么接口B可以賦值給接口A。
幾個接口也可以組合出一個接口.
type ReadWriter interface {
Reader
Writer
}
等價于:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
-
Any類型
由于Go語言中任何對象實例都滿足接口interface{},所以interface{}看起來是任何對象的Any類型
var v1 interface{} = 1
var v2 interface{} = "abc"
var v3 interface{} = &v2
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}
func Printf(fmt string, args ...interface{})
func Println(args ...interface{})
-
接口轉(zhuǎn)換和類型查詢
接口轉(zhuǎn)換
package main
import(
"fmt"
)
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
//定義接口
type LessAdder interface {
Less(b Integer) bool //函數(shù)聲明
Add(b Integer) //函數(shù)聲明
}
//定義接口
type Adder interface {
Add(b Integer) //函數(shù)聲明
}
func main() {
var a Integer = 10
var b LessAdder = &a //道理我們前面提到過了,Add接收者是個對象指針
if c , ok = b.(Adder); ok{
c.Add(10)
fmt.Println(a)
//fmt.Println(c.Less(100)) //報錯,c.Less undefined (type Adder has no field or method Less)
}
}
類型查詢
package main
import(
"fmt"
)
func main() {
b := "a"
var a interface{} = b
switch a.(type) {
case int:
fmt.Println("int")
case float32:
fmt.Println("float32")
case int32:
fmt.Println("int32")
case float64:
fmt.Println("float64")
case bool:
fmt.Println("bool")
case string:
fmt.Println("string")
default:
fmt.Println("ok")
}
}
-
補充
我們補充一些defer-panic-recover的案列.
package main
import (
"fmt"
)
func f() {
fmt.Println("a")
}
func main() {
defer func() {
if err := recover(); err != nil{
fmt.Println(err)
}
}()
f()
fmt.Println("b")
}
輸出結果:
a
b
如果我們在f中panic呢? 這會發(fā)生什么呢?
package main
import (
"fmt"
)
func f() {
fmt.Println("a")
panic("error!")
}
func main() {
defer func() {
if err := recover(); err != nil{
fmt.Println(err)
}
}()
f()
fmt.Println("b")
}
輸出:
a
error!
我們發(fā)現(xiàn), b沒有輸出. panic會拋出一個異常, 由recover去捕獲.f拋出異常后, 事實上,
main剩下的部分都不會執(zhí)行, 但是因為我們defer了,
defer是一定會執(zhí)行的,因此我們在defer中捕獲了panic拋出的
異常. 這就是為什么b沒有輸出. 似乎和try catch很像. 如果我們希望把b也輸出, 但也能捕獲異常呢?
package main
import (
"fmt"
)
func f() {
fmt.Println("a")
panic("error!")
}
func main() {
func(){
defer func() {
if err := recover(); err != nil{
fmt.Println(err)
}
}()
f()
}()
fmt.Println("b")
}
輸出結果:
a
error!
b
如果是這樣呢?
package main
import (
"fmt"
)
func f() {
fmt.Println("a")
panic("error!")
}
func main() {
func(){
f()
defer func() {
if err := recover(); err != nil{
fmt.Println(err)
}
}()
}()
fmt.Println("b")
}
此時, 在定義defer之前, f已經(jīng)panic了, 沒有recover去捕獲, 這個panic會一直拋出.
直到被go虛擬機捕獲.
輸出:
a
panic: error!
goroutine 1 [running]:
main.f()
/Users/fengyan/code/go/test/main.go:9 +0x11e
main.main.func1()
/Users/fengyan/code/go/test/main.go:14 +0x18
main.main()
/Users/fe
go里面有個東西很好玩, nil類似于java的null, 那么java如果對null調(diào)用方法, 會直接拋出一個空指針異常.
那么go會怎么樣呢?
package main
func main() {
nil.Println("a")
}
輸出結果:
# command-line-arguments
./main.go:4: use of untyped nil
看來還是不行的.
所以調(diào)用前我們還是要進行空的判斷.
三. go并發(fā)編程
并發(fā)不是并行. 并發(fā)比并行更加優(yōu)秀. 并發(fā)是時間片輪換, 并行是多核計算. 事實上, 并行可以由并發(fā)指定到多個cpu執(zhí)行.
我們馬上看一個具體的例子.
package main
import (
"fmt"
)
func Add(x, y int) {
z := x + y
fmt.Println(z)
}
func main() {
for i := 0; i < 10; i++ {
go Add(i,i)
}
}
結果我們發(fā)現(xiàn)什么都沒有輸出, 這是因為go是異步執(zhí)行的, main不會等Add完成, 它會繼續(xù)執(zhí)行下去, 于是發(fā)生了main
函數(shù)先結束, 于是進程就結束了.
所以這里我們需要做同步, 所謂同步, 就是主線程去等待子線程完成(go里的線程其實是協(xié)程, 協(xié)程你可以認為是輕量級的線程).
線程間通信模型基本上常用的也就是兩種,共享內(nèi)存和消息隊列.
后者目前看來比較流行, 比如akka(actor模型), zmq(消息隊列模型)等 都是基于消息隊列的并發(fā)模型.
go也是采用了消息隊列的方式, 這里就是channel.
channel的申明形式:
var chanName chan ElementType
ElementType是該chan能夠支持的數(shù)據(jù)類型.
chan的初始化有兩種:
ch := make(chan int)
和
ch := make(chan int,capacity)
前者寫入和讀取是阻塞的, 后者自帶了一個buffer,
如果沒有達到capacity, 是非阻塞的, 達到capacity才會阻塞. 讀取的話, 如果為buffer空,
會阻塞.
chan寫入數(shù)據(jù)
ch <- value
chan讀取數(shù)據(jù)
value : = <-ch
對于帶buffer的chan也可以用for和range讀取:
for i := range ch {
fmt.Println("Received:", i)
}
下面我們把同步后的代碼貼上:
package main
import (
"fmt"
)
func Add(x, y int, ch chan int) {
z := x + y
fmt.Println(z)
//寫入后就阻塞
ch <- 1
}
func main() {
//chan int slice, make([]chan int, 10)是創(chuàng)建slice的方法
//容量是10,超過這個容量,會發(fā)生內(nèi)存拷貝
//var chs []chan int = make([]chan int, 10)
//其實這里用數(shù)組更好
var chs [10]chan int
for i := 0; i < 10; i++ {
chs[i] = make(chan int)
go Add(i,i,chs[i])
}
//同步
for _, ch := range chs{
//如果線程沒有寫入數(shù)據(jù), 會阻塞
<- ch
}
}
輸出結果:
18
0
2
4
12
14
16
10
8
6
下面我們介紹一下select語法,
select {
case <-chan1:
//讀取數(shù)據(jù)
case chan2 <- 1:
//寫入數(shù)據(jù)
default:
//默認操作
}
select會去注冊事件, 比如是否可讀,是否可寫, 根據(jù)對應的事件去執(zhí)行相應的代碼.
package main
import (
"fmt"
)
func RandomSignal(ch chan int) {
for{
select{
//寫入1事件
case ch <- 1:
//寫入0事件
case ch <- 0:
}
}
}
func main() {
//var ch chan int = make(chan int)效果一樣
//但是有些微妙的不同
var ch chan int = make(chan int,1)
go RandomSignal(ch)
for value := range ch{
fmt.Println(value)
}
}
這是一個隨機產(chǎn)生0或者1的信號發(fā)生器.
單向channel
顧名思義,單向channel只能用于發(fā)送或者接收數(shù)據(jù)。channel本身必然是同時支持讀寫的,
否則根本沒法用。假如一個channel真的只能讀,那么肯定只會是空的,因為你沒機會往里面寫數(shù)
據(jù)。同理,如果一個channel只允許寫,即使寫進去了,也沒有意義,因為沒有機會讀取里面
的數(shù)據(jù)。所以的單向channel概念,其實只是對channel的一種使用限制。
var ch1 chan int // 雙向
var ch2 chan<- float64// 只寫
var ch3 <-chan int // 只讀
那么單向channel如何初始化呢?之前我們已經(jīng)提到過,channel是一個原生類型,因此不僅
支持被傳遞,還支持類型轉(zhuǎn)換。只有在有了單向channel的概念后,讀者才會明白類型轉(zhuǎn)換對于
channel的意義:就是在單向channel和?向channel之間進行轉(zhuǎn)換。示例如下:
ch4 := make(chan int)
ch5 := <-chan int(ch4) // 只讀
ch6 := chan<- int(ch4) //只寫
關閉channel
close(ch)
如何判斷一個channel是否已經(jīng)被關
閉?我們可以在讀取的時候使用多重返回值的方式:
x, ok := <-ch
package main
import (
"fmt"
)
func RandomSignal(ch chan int) {
for i:= 0; i <= 9; i++{
select{
//寫入1事件
case ch <- 1:
//寫入0事件
case ch <- 0:
}
}
close(ch)
}
func main() {
//var ch chan int = make(chan int)效果一樣
//但是有些微妙的不同
var ch chan int = make(chan int,1)
go RandomSignal(ch)
for value := range ch{
fmt.Println(value)
}
}
輸出:
1
0
1
1
0
1
1
0
0
0
close(ch)之后, 對ch的for循環(huán)會終止.
并行計算:
package main
import (
"runtime"
"sync"
"fmt"
)
type Vector []float64
func min(a int, b int) int {
if a < b {
return a
}
return b
}
func max(a int, b int) int {
if a < b {
return b
}
return a
}
func mul(u, v Vector, k int) (res float64) {
n := min(k+1, len(u))
j := min(k, len(v)-1)
for i := k - j; i < n; i, j = i+1, j-1 {
res += u[i] * v[j]
}
return
}
func Convolve(u, v Vector) (w Vector) {
n := len(u) + len(v) - 1
w = make(Vector, n)
size := max(1, 1<<20/n)
wg := new(sync.WaitGroup)
wg.Add(1 + (n-1)/size)
for i := 0; i < n && i >= 0; i += size {
j := i + size
if j > n || j < 0 {
j = n
}
go func(i, j int) {
for k := i; k < j; k++ {
w[k] = mul(u, v, k)
}
wg.Done()
}(i, j)
}
wg.Wait()
return
}
func main() {
//numcpu := runtime.NumCPU()
numcpu := 2
runtime.GOMAXPROCS(numcpu) //設置使用多少個cpu核心
var a Vector = make([]float64,1000000)
var b Vector = make([]float64,1000000)
for i := 0; i < 1000000 - 1; i++ {
a[i] = 1
b[i] = 1
}
w := Convolve(a,b)
fmt.Println(w)
}
runtime.GOMAXPROCS(numcpu)
該函數(shù)是用來設置cpu使用的核心個數(shù), 在許的書中, 如果不設置這個環(huán)境變量, 默認是1個核心, 但是事實上,
在我這個版本的go語言中, 已經(jīng)支持默認多核并發(fā)啦.
下面我們來看看同步的概念.
Go語言包中的sync包提供了兩種?類型:
sync.Mutex和sync.RWMutex。sync.Mutex
是讀寫都鎖, sync.RWMutex是寫鎖讀不鎖.
用法:
var l sync.Mutex
func foo() {
l.Lock()
defer l.Unlock()
//...
}
對于從全局的角度只需要運行一次的代碼,比如全局初始化操作,Go語言提供了一個Once
類型來保證全局的唯一性操作,具體代碼如下:
var a string
var once sync.Once
func setup() {
a = "hello, world"
}
func doprint() {
once.Do(setup)
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
如果不考慮線程安全, 等價于:
var done bool = false //全局變量
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
setup()
}
print(a)
}
WaitGroup
var wg sync.WaitGroup
該類型有三個指針方法,即Add、Done和Wait。
類型sync.WaitGroup是一個結構體類型。在它之中有一個代表計數(shù)的字段。
當一個sync.WaitGroup類型的變量被聲明之后,其值中的那個計數(shù)值將會是0。
我們可以通過該值的Add方法增大或減少其中的計數(shù)值。
雖然Add方法接受一個int類型的值,并且我們也可以通過該方法減少計數(shù)值,但是我們一定不要讓計數(shù)值變?yōu)樨摂?shù)。因為這樣會立即引發(fā)一個運行恐慌。
這也代表著我們對sync.WaitGroup類型值的錯誤使用。
除了調(diào)用sync.WaitGroup類型值的Add方法并傳入一個負數(shù)之外,我們還可以通過調(diào)用該值的Done來使其中的計數(shù)值減一。
當我們調(diào)用sync.WaitGroup類型值的Wait方法的時候,它會去檢查該值中的計數(shù)值。
如果這個計數(shù)值為0,那么該方法會立即返回,且不會對程序的運行產(chǎn)生任何影響。 但是,如果這個計數(shù)值大于0,
那么該方法的調(diào)用方所屬的那個Goroutine就會被阻塞。
直到該計數(shù)值重新變?yōu)?之時,為此而被阻塞的所有Goroutine才會被喚醒。
我們來看第一個例子:
package main
import (
"fmt"
"sync"
)
func Add(x, y int, wg *sync.WaitGroup) {
z := x + y
fmt.Println(z)
wg.Done()
}
func main() {
var wg *sync.WaitGroup = &sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go Add(i,i,wg)
}
wg.Wait()
}
初級教程我們就這樣快速結束了, 咱們高級見. 嗯, 233333333.

浙公網(wǎng)安備 33010602011771號