GO核心編程
簡介
go語言特點:
- go具有垃圾回收機制
- 從語言層面支持並發,goroutine,高效利用多核,基於CPS並發模型實現(重要特點)
- 吸收了管道通信機制,實現不同goroutine之間的互相通信
- 函數可以返回多個值
- 切片、延時執行defer
- 繼承C語言很多思想,引入包的概念,用於組織程序結構
golang執行流程分析
第一種方式是go build編譯后生成可執行文件,在運行可執行文件即可;第二種方式是直接go run源文件。兩種方式的區別:
- 如果我們先編譯生成了可執行文件,那么我們可以將該可執行文件拷貝到沒有 go 開發環境的機器上,仍然可以運行
- 如果我們是直接 go run go 源代碼,那么如果要在另外一個機器上這么運行,也需要 go 開發環境,否則無法執行
- 在編譯時,編譯器會將程序運行依賴的庫文件包含在可執行文件中,所以,可執行文件變大了很多
真正工作時候需要先編譯在運行!!
go程序開發注意事項
- Go每個語句后不需要分號(Go語言會在每行后自動加分號)
- Go編譯器是一行行進行編譯的,一行只能寫一條語句
- 存在未使用的包或變量,編譯會通不過
規范代碼風格
編寫完代碼后可以通過gofmt -w main.go
來進行格式化;Go設計者的思想:一個問題盡量只有一個解決方法。
基本語法
變量使用注意事項
-
如果一次聲明多個全局變量
var( n3 = 300 name = "mary" ) //局部變量 var n3, name = 300, "mary"
-
//查看變量類型和占用字節 fmt.Printf("n1 的 類型 %T\n n1占用的字節數 %d",n1,unsafe.Sizeof(n1))
-
/* byte~uint8 存儲字符時候選用byte 如果保存字符對應碼值大於255,比如漢字,可以考慮使用int類型保存 如果需要輸出字符,需要格式化輸出 */ //rune~int32 表示一個Unicode碼
-
/* Go中字符串是不可變的 字符串兩種表現形式: 雙引號:會識別轉義字符 反引號:以字符串的原生形式輸出,包括換行和特殊字符,可以實現防止攻擊、輸出源代碼等效果 */
-
Go數據類型不能自動轉換,需要顯示轉換T(v)
//基本數據類型和string的相互轉換 //Sprintf會根據format參數生成格式化的字符串並返回該字符串
-
go語言不支持三元運算符
流程控制使用注意事項
- Switch...case語句中,case后面不再需要添加break,case后面也可以有多個值,用逗號分隔開。如果想要執行下面的語句,添加
fallthrough
關鍵字,叫做switch穿透 - 循環遍歷只有一個for關鍵字,可以用for range語句來遍歷數組。
包使用注意事項
- 一個文件夾下的所有.go文件同屬於一個包,一般和文件夾一樣。在同一個包下不能有相同的函數名和變量名,即使在不同文件中也一樣。
- 跨包訪問的函數或變量首字母需要大寫,相當於public。
- import實際上是import "文件夾名字",訪問時候是用的包名.函數名,因為包名可以和文件夾名不一樣
- 如果要編譯成一個可執行程序文件,就需要將這個包聲明為main;如果是寫一個庫,包名可以自定義
函數使用注意事項
-
基本數據類型和數組默認都是值傳遞
-
Go中,函數也是一種數據類型,可以賦給一個變量,類似於C語言的函數指針,類型為func(type1,type2)
-
C++中typedef,在Go中變為type
-
支持對函數返回值命名
func getSumAndSub(n1 int,n2 int)(sum int,sub int){ sum = n1 + n2 sub = n1 - n2 return }
-
支持可變參數
func sum(args... int) sum int{ } func sum(n1 int,args... int) sum int{ } //args是slice切片,通過args[index]可以訪問到各個值,可變參數要放在形參列表最后
-
每一個源文件都可以包含一個 init 函數,該函數會在 main 函數執行前,被 Go 運行框架調用,也 就是說 init 會在 main 函數前被調用。如果一個文件同時包含全局變量定義,init 函數和 main 函數,則執行的流程全局變量定義->init函數->main 函數,
如果import其他文件,則先執行其他文件的初始化
!!! -
匿名函數
//方式一 res1:= func(n1 int) int{ return n1+1 }(10) //方式二 fun:=func(n1 int) int{ return n1+1 } res2:=fun(10)
-
閉包
閉包就是一個函數和與其相關的引用環境組合的一個整體.可以這樣理解: 閉包是類, 函數是操作,n 是字段。函數和它使用到 n 構成閉包。
要搞清楚閉包的關鍵,就是要分析出返回的函數它使用(引用)到哪些變量,因為函數和它引
用到的變量共同構成閉包
func makeSuffix(suffix string) func(string) string{ return func(name string) string{ //如果name沒有指定后綴,則加上,否則就返回原來的名字 if !strings.HasSuffix(name,suffix){ return name+suffix } } } f2 := makeSuffix(".jpg") fmt.Println(f2("winter")) //winter.jpg fmt.Println(f2("bird.jpg")) //bird.jpg
我們體會一下閉包的好處,如果使用傳統的方法,也可以輕松實現這個功能,但是傳統方法需要每 次都傳入 后綴名,比如 .jpg ,而閉包因為可以保留上次引用的某個值,所以我們傳入一次就可以反復 使用。這個makeSuffix用處有點類似於java的泛型和c++的模版類,生成特定后綴判斷的函數變量
-
defer
當執行到defer時,暫停不執行,會將defer后面的語句壓入到獨立的棧(defer棧),當函數執行完畢后,再從defer棧中取出語句執行,在defer語句放入到棧時,也會將相關的值拷貝同時入棧
func sum(n1 int,n2 int) int{ defer fmt.Println("ok1 n1=",n1) defer fmt.Println("ok2 n2=",n2) n1++ n2++ res:=n1+n2 fmt.Println("ok3 res=",res) return res } //執行結果 //ok3 res=32 //ok2 n2=20 //ok1 n1=10
defer 最主要的價值是在,當函數執行完畢后,可以及時的釋放函數創建的資源
-
函數傳參
值類型:基本數據類型、數組和結構體 struct,默認是值傳遞
引用類型:指針、slice 切片、map、管道 chan、interface 等,默認是引用傳遞
-
錯誤處理
Go語言不支持傳統的try...catch...finally處理,引入的處理方式為defer,panic,recover。
這幾個異常的使用場景可以這么簡單描述:Go 中可以拋出一個 panic 的異常,然后在 defer 中通過 recover 捕獲這個異常,然后正常處理。
func test(){ defer func(){ err := recover() //recover()內置函數,可以捕獲到異常 if err != nil{ fmt.Println("err",err) } }() num1 := 10 num2 := 0 res := num1/num2 fmt.Println("res=",res) }
自定義錯誤:
1.errors.New("錯誤說明") , 會返回一個 error 類型的值,表示一個錯誤
2.panic 內置函數 ,接收一個 interface{}類型的值(也就是任何值了,相當於java的Object)作為參數。可以接收 error 類型的變量,輸出錯誤信息,並退出程序.
數組和切片
go語言中數組的名字不在是地址了,數組的首地址為&arr或者&arr[0]。
var arr1 = [3]int{5,6,7} //var slice = []int{1,2,3} 雖然可以這樣,但已經不是一個數組了,數組聲明必須指定長度
var arr2 = [...]int{1,3,3}
var arr3 = [...]int{1:800,0:900,2:999}
//for range遍歷方式
for index,value range arr1{
}
數組使用注意事項
-
func test(arr [3]int){ //值傳遞,不影響原來的 } func test(arr *[3]int){//可以通過傳指針 } //Go語言傳參有嚴格的限制,[3]int類型和[4]int類型不一致!!!
-
二維數組定義后面的賦值必須嚴格的划分開,不能省略花括號!!
arr := [2][2]int{{1,2},{3,4}} arr := [...][2]int{{1,2},{3,4}} //二維數組for-range遍歷 for i,v:= range arr{ for j,v2:=range v{ } }
切片是數組的一個引用,因此切片是引用類型,是一個可以動態變化的數組。
slice := ar[1:3] //左開右閉
slice := make([]int,len,[cap])
slice := []int{1,2,3}
方式一和方式二的區別
通過 make 方式創建的切片對應的數組是由 make 底層維護,對外不可見,即只能通過 slice 去訪問各個元素。方式一直接飲用數組,這個數組事先存在,程序員可見。
切片使用注意事項
- 切片可以繼續切片,因為切片的更改會影響底層數組的更改。
- append內置函數可以對切片追加具體元素,也可以追加slice。追加的具體元素如果不超過底層數組的長度,則會覆蓋底層數組的數值;當超過底層數組的長度時候,go會創建一個新的數組,將slice原來包含的元素拷貝到新的數組然后重新引用newArr。
- 內置函數copy(dest,source)的參數類型是切片,source長度可以閉dest大
string和slice
-
string底層是一個byte數組,因此string也可以進行切片
-
string是不可變的,
str[0]='z'
編譯不通過//如果想要改變,可以現將string轉成byte切片,修改完后在轉為string arr1 := []byte(str) arr1[0] = 'z' str = string(arr1) //這種轉換僅僅適用於string <---> byte,可以把byte當成char類型
注意:當我們轉成[]byte后,可以處理英文和數字,不能處理中文,因為一個漢字3個字節,會出現亂碼,解決辦法是將string轉成[]rune即可,因為[]rune是按字符處理的,兼容漢字。
map
聲明一個map是不會分配內存的,初始化需要make,分配內存后才能賦值和適用。
m := make(map[string]string,10) //容量達到后,會自動擴容
m := make(map[string]string)
新增操作:Map["key"]=value 如果key還沒有就是增加,如果key存在就是修改。
刪除操作:delete(map,"key"),如果一次性刪除所有的則需要一個個遍歷key去delete
查找操作:v,ok :=map["tom"]
Slice of map
m := make([]map[string]string,2) //類型為map[string]string的切片,大小為2,第三個map就需要先append在使用了,否則會越界
//切片的數據類型如果是 map,則我們稱為 slice of map,map 切片,這樣使用則 map 個數就可以動態變化了
注意:使用slice和map一定要先make
map中的key是無序的,每次遍歷得到的結果可能不一樣,Go沒有辦法對map進行排序,但是有辦法根據key來順序輸出map。
/*
1. 先將map的key放到切片中
2. 對切片排序
3. 遍歷切片,然后按照key來輸出map值
*/
面向對象編程
結構體
type Person struct{
}
p := Person{"mary",20}
var person *Person = new(Person)
(*person).Name = "smith" //person.Name = "smith"
//go設計者為了程序員使用方便,底層會對person.Name進行處理,加上(*person).Name
結構體使用注意細節:
- 不同結構體可以相互轉換,前提是需要有完全相同的字段(名字、個數和類型)
- struct 的每個字段上,可以寫上一個 tag, 該 tag 可以通過反射機制獲取,常見的使用場景就是序列化和反序列化。
方法
func (p Person) test(){
//...
}
方法使用注意事項
- Golang 中的方法是作用在指定的數據類型上的(即:和指定的數據類型綁定),因此自定義類型,都可以有方法,而不僅僅是 struct,int,floate32等都可以有方法
- 變量調用方法時,該變量本身也會作為一個參數傳遞到方法(如果變量是值類型,則進行值拷貝,如果變量是引用類型,則進行地址拷貝
- 方法的訪問范圍控制的規則,和函數一樣。方法名首字母小寫,只能在本包訪問,方法首字母 大寫,可以在本包和其它包訪問
- 如果一個類型實現了 String()這個方法,那么 fmt.Println 默認會調用這個變量的 String()進行輸 出
工廠模式
問題來了,如果首字母是小寫的, 比如 是 type student struct {....} 就不不行了,怎么辦---> 工廠模式來解決.
type student struct{
Name string
score float64
}
func NewStudent(n string,s float64) *student{
return &student{
Name:n,
Score:s,
}
}
//首字母小寫的字段也不能跨包訪問,需要提供一個方法
func (s *student) GetScore() float64{
return s.score
}
封裝
在 Golang 開發中並沒有特別強調封裝,這點並不像 Java
- 將結構體、字段(屬性)的首字母小寫(不能導出了,其它包不能使用,類似 private)
- 給結構體所在包提供一個工廠模式的函數,首字母大寫。類似一個構造函數
- 提供一個首字母大寫的 Set /Get方法(類似其它語言的 public)
繼承
在 Golang 中,如果一個 struct 嵌套了另一個匿名結構體,那么這個結構體可以直接訪問匿名結構體的字段和方法,從而實現了繼承特性,也即匿名結構體的所有東西成為了新的結構體的一部分。
- 結構體可以使用匿名結構體的所有字段和方法,大小寫都可以,但是要在同一個包里面去訪問。、
- 匿名結構體字段訪問可以簡化,比如b.A.age=19可以寫b.age=19。
- 當結構體和匿名結構體有相同的字段或者方法時,編譯器采用就近訪問原則訪問,如希望訪問匿名結構體的字段和方法,可以通過匿名結構體名來區分
- 結構體嵌入兩個(或多個)匿名結構體,如兩個匿名結構體有相同的字段和方法(同時結構體本身 沒有同名的字段和方法),在訪問時,就必須明確指定匿名結構體名字,否則編譯報錯
- 如果一個 struct 嵌套了一個有名結構體,這種模式就是組合,如果是組合關系,那么在訪問組合的結構體的字段或方法時,必須帶上結構體的名字
- 如一個 struct 嵌套了多個匿名結構體,那么該結構體可以直接訪問嵌套的匿名結構體的字段和方法,從而實現了多重繼承。盡量不要使用多重繼承
接口
Go采用接口來實現多態,interface 類型可以定義一組方法,但是這些不需要實現。並且 interface 不能包含任何變量。只要一個變量,含有接口類型中的所有方法(注意:一定要是所有),那么這個變量就實現這個接口。
接口使用注意事項
-
空接口 interface{} 沒有任何方法,所以所有類型都實現了空接口, 即我們可以把任何一個變量賦給空接口
-
只要是自定義數據類型,就可以實現接口
type integer int func (i integer) say{ //... }
類型斷言
接口要轉成具體類型就要用到類型斷言
var x interface{}
var b2 float32 = 1.1
x = b2
y := x.(float32) //arg.(type)
在進行類型斷言時,如果類型不匹配,就會報 panic, 因此進行類型斷言時,要確保原來的空接口指向的就是斷言的類型
如何在進行斷言時,帶上檢測機制,如果成功就 ok,否則也不要報 panic
if y,ok := x.(float32);ok{
//convert success
}else{
//convert fail
}
高級教程
命令行參數
os.Args 是一個 string 的切片,用來存儲所有的命令行參數
for i,v:= range os.Args{
fmt.Printf("args[%v]=%v\n",i,v)
}//有效參數從Args[1]開始,即第二個
flag包解析命令行參數
前面的方式是比較原生的方式,對解析參數不是特別的方便,特別是帶有指定參數形式的命令行。go 設計者給我們提供了 flag 包,可以方便的解析命令行參數,而且參數順序可以隨意。
//定義幾個變量,用於接受命令行參數
var user string
var pwd int
flag.StringVar(&user,"u","","用戶名,默認為空")
flag.IntVar(&pwd,"pwd",0,"密碼,默認為空")
flag.Parse()
fmt.Printf("user=%v pwd=%v\n",user,pwd)
序列化和反序列化
json.Marshal(v interface{}) ([]byte,error) //序列化
type monster struct{
}
json.unMarshal([]byte(str),&monster) //序列化
對於結構體的序列化,如果我們希望序列化后的 key 的名字,又我們自己重新制定,那么可以給 struct指定一個 tag 標簽。
在反序列化一個json字符串時,要確保反序列化后的數據類型和原來序列化前的數據類型一致
*單元測試
Go 語言中自帶有一個輕量級的測試框架 testing 和自帶的 go test 命令來實現單元測試和性能測試.testing 框架和其他語言中的測試框架類似,可以基於這個框架寫針對相應函數的測試用例,也可以基於該框架寫相應的壓力測試用例。
- 測試用例文件名必須以 _test.go 結尾。 比如 cal_test.go , cal 不是固定的
- 測試用例函數必須以 Test 開頭,一般來說就是 Test+被測試的函數名,比如 TestAddUpper
- TestAddUpper(t *tesing.T) 的形參類型必須是 *testing.
- 當出現錯誤時,可以使用 t.Fatalf 來格式化輸出錯誤信息,並退出程序,t.Logf 方法可以輸出相應的日志
goroutine
Go 主線程(有程序員直接稱為線程/也可以理解成進程): 一個 Go 線程上,可以起多個協程,你可以這樣理解,協程是輕量級的線程[編譯器做優化]。(這里只是叫法發生了變化)
Go可以輕輕松松的起上萬個協程。
channel
這個解決的是不同的goroutine如何通信的問題。
全局變量的互斥鎖
lock sync.Mutex
lock.lock
//...
lock.unlock
上面這種方法不完美,主線程在等待所有 goroutine 全部完成的時間很難確定;
如果主線程休眠時間長了,會加長等待時間,如果等待時間短了,可能還有 goroutine 處於工作狀態,這時也會隨主線程的退出而銷毀;
通過全局變量加鎖同步來實現通訊,也並不利用多個協程對全局變量的讀寫操作
在運行某個程序時,如何知道是否存在資源競爭問題。 方法很簡單,在編譯該程序時,增加一個參數 -race 即可
-
channel本質就是一個數據結構-隊列,它是有類型的,是線程安全的(多個協程操作同一個管道時,不會發生資源競爭問題)
-
channel必須初始化才能寫入數據,即make后才能使用
var intChan chan int intChan = make(chan int,3)
-
當我們給管寫入數據時,不能超過其容量,它的價值是一邊放一邊取
-
allChan := make(chan interface{},3) allChan <- Cat{Name:"tom",Age:18} newCat <- allChan fmt.Printf("%T\n%v",newCat,newCat) //正常輸出 fmt.Printf("newCat.Name=%v",newCat.Name) //編譯不通過!!! a := newCat.(Cat) //使用類型斷言!!!!
-
使用內置函數 close 可以關閉 channel, 當 channel 關閉后,就不能再向 channel 寫數據了,但是仍然 可以從該 channel 讀取數據,只有關閉后讀完會自動退出
-
在沒有使用協程的情況下,如果 channel 數據取完了,再取就會報 dead lock ,寫也是一樣。使用協程則會阻塞。
應用實例1
一個讀協程,一個寫協程,操作同一管道,主線程需要等待兩個協程都完成工作才能退出。
func writeData(intChan chan int){
for i:=1;i<=50;i++ {
//放入數據
intChan <- i
fmt.Println("write data",i)
}
close(intChan)
}
func readData(intChan chan int,exitChan chan bool){
for{
v,ok := <-intChan
if !ok {
break
}
fmt.Println("read data=%v\n",v)
}
//任務完成
exitChan<-true
close(exitChan)
}
func main() {
//創建兩個管道
intChan := make(chan int,10)
exitChan := make(chan bool,1)
go writeData(intChan)
go readData(intChan,exitChan)
for{
_,ok := <- exitChan
if !ok {
break
}
}
}
管道的阻塞機制
如果只是向管道寫入數據,而沒有讀取數據,就會出現阻塞而dead lock。原因是intChan容量是10,而代碼wirteData會寫入50個數據,因此會阻塞在writeData的ch<-i。但是寫管道和讀管道的頻率不一致,無所謂
應用實例2
統計1-8000的數字中,哪些是素數?將統計素數的任務分配給4個goroutine去完成
//向intChan放入1-8000個數
func putNum(intChan chan int){
for i:=1;i<=8000;i++ {
intChan <- i
}
close(intChan)
}
//從intChan取出數據,並判斷是否為素數,如果是,就放入primeChan
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool) {
var flag bool
for {
time.Sleep(time.Microsecond*10)
//取一個數處理
num,ok := <-intChan
if !ok{
break
}
flag = true
//判斷
for i:=2;i<num;i++{
if num%2 ==0 {
flag=false
break
}
}
//放入
if flag {
primeChan <- num
}
}
fmt.Println("有一個primeNum協程因為取不到數據,退出")
//這里不能關閉primeChan
exitChan <- true
}
func main(){
intChan := make(chan int, 1000)
primeChan := make(chan int,2000)
exitChan := make(chan bool,4) //4個
go putNum(intChan)
//開啟4個協程,從intChan取出數據判斷
for i:=0;i<4;i++{
go primeNum(intChan,primeChan,exitChan)
}
go func() {
for i:=0;i<4;i++{
<-exitChan
}
//當我們從exitChan取出4個結果,就可以放心關閉primeChan
close(primeChan)
}()
for {
res,ok := <-primeChan
if !ok{
break
}
fmt.Println("%d\n",res)
}
}
channel使用注意事項
-
channel可以聲明為只讀或者只寫,可以有效防止誤操作,降低權限。
/* var writeChan chan<-int //只寫 var readChan <-chan int //只讀 */
-
傳統方法在遍歷管道時,如果不關閉后阻塞而導致deadlock。在實際開發中,可能我們不好確定什么時候關閉管道,使用select可以解決從管道取數據的阻塞問題。
for{ //select里面的case是並發執行的 select{ //這里如果intChan一直沒有關閉,不會一直阻塞而deadlock,沒有數據的話會自動到下一個case匹配 case v:= <-intChan fmt.Printf("從intChan讀取的數據%d\n",v) case v:= <-stringChan fmt.Printf("從stringChan讀取的數據%d\n",v) default: fmt.Printf("都取不到,程序員可以加入邏輯\n") time.Sleep(time.Second) return } }
-
goroutine中使用recover,解決協程中出現panic,導致程序崩潰問題。
反射
反射可以在運行時動態獲取變量的各種信息, 比如變量的類型(type),類別(kind),如果是結構體變量,還可以獲取到結構體本身的信息(包括結構體的字段、方法),通過反射,可以修改變量的值,可以調用關聯的方法。
反射常見的應用場景
- 不知道接口調用哪個函數,根據傳入參數在運行時確定調用的具體接口,這種需要對函數或方法反射。
- 對結構體序列化時,如果結構體有指定tag,也會使用反射生成對應的字符串
概念
-
reflect.TypeOf()/reflect.ValueOf()
-
變量、interface{}、reflect.Value是可以相互轉換的
func reflectTest(b interface{}) { //通過反射獲取傳入變量的type,kind,值 rType := reflect.TypeOf(b) fmt.Println("rType=",rType) rVal := reflect.ValueOf(b) n:= 2+rVal.Int() fmt.Println("n=",n) fmt.Printf("rVal=%v rVal type=%T\n",rVal,rVal) iV:=rVal.Interface() n2:=iV.(int) fmt.Println("n2=",n2) }
反射使用注意細節
- Reflect.Vlaue.Kind獲取變量的類別,返回一個常量,type和kind有時候一樣有時候不一樣,stu Type是Student,Kind是struct
- 通過反射的來修改變量, 注意當使用 SetXxx 方法來設置需要通過對應的指針類型來完成, 這樣才能改變傳入的變量的值, 同時需要使用到 reflect.Value.Elem()方法(相當於獲取指針指向變量的值)
反射最佳實踐
使用反射遍歷結構體的字段,調用結構體的方法,並獲取結構體標簽的值
type Monster struct{
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32 `json:"成績"`
Sex string
}
func (s Monster) GetSum(n1,n2 int) int {
return n1+n2
}
func (s Monster) Set(name string,age int,score float32,sex string){
s.Name=name
s.Age=age
s.Score=score
s.Sex=sex
}
func (s Monster) Print(){
fmt.Println("----start---")
fmt.Println(s)
fmt.Println("-----end-----")
}
func TestStruct(a interface{}){
typ := reflect.TypeOf(a)
rval := reflect.ValueOf(a)
kd := rval.Kind()
if kd != reflect.Struct{ //如果不是struct就退出
fmt.Println("expect struct")
return
}
//獲取結構體有幾個字段
num := rval.NumField()
fmt.Printf("structs has%d fileds\n",num)
for i:=0;i<num;i++{
fmt.Printf("Filed %d值為%v\n",i,rval.Field(i))
tagVal := typ.Field(i).Tag.Get("json")
//如果該字段有tag就顯示,否則就不顯示
if tagVal !=""{
fmt.Printf("File%d:tag為%v",i,tagVal)
}
}
//獲取結構體有多少個方法
numOfMethod:=rval.NumMethod()
fmt.Printf("struct has %d methods\n",numOfMethod)
//方法的排序默認是按照函數名排序
rval.Method(1).Call(nil)//獲取到第二個方法即Print,調用它,因此沒有參數
//調用結構體的第一個方法Method(0)
var params []reflect.Value
params = append(params,reflect.ValueOf(10))
params = append(params,reflect.ValueOf(40))
res:=rval.Method(0).Call(params)//傳入參數是[]reflect.Value
fmt.Println("res=",res[0].Int())//返回結果是[]reflect.Value
}
TCP編程
端口分類:0保留端口;1-1024固定端口;1025-65525動態端口,程序員可以使用,一個端口只能被一個程序監聽,服務器要盡可能少用端口。
服務端代碼
func process(conn net.Conn){
defer conn.Close()
for{
buf:=make([]byte,1024)
//等待客戶端conn發送信息,如果客戶端沒有發送,那么協程就阻塞在這里
fmt.Printf("服務器在等待客戶端%s 發送信息\n",conn.RemoteAddr().String())
n,err := conn.Read(buf)
if err != nil{
fmt.Printf("客戶端退出 err=%v",err)
return //!!!
}
//顯示客戶端發送的內容到服務器的終端
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服務器開始監聽...")
listen,err:= net.Listen("tcp","0.0.0.0:8888")
if err!= nil{
fmt.Println("listen err=",err)
return
}
defer listen.Close() //延時關閉listen
//循環等待客戶端來連接我
for{
fmt.Println("等待客戶端來連接...")
conn,err:=listen.Accept()
if err != nil{
fmt.Println("Accept() err=",err)
}else{
fmt.Printf("Accept() success con=%v 客戶端ip=%v\n",conn,conn.RemoteAddr().String())
}
//這里准備一個協程為客戶端服務
go process(conn)
}
}
客戶端代碼
func main(){
conn,err := net.Dial("tcp","0.0.0.0:8888")
if err != nil{
fmt.Println("client dial err=",err)
return
}
//客戶端可以發送單行數據,然后就退出
reader := bufio.NewReader(os.Stdin)
for {
//從終端讀一行用戶輸入,並准備發送給服務器
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
//如果用戶輸入的是exit就退出
line = strings.Trim(line, " \r\n")
if line == "exit" {
fmt.Println("客戶端退出..")
break
}
//再將line發送給服務器
_, err = conn.Write([]byte(line + "\n"))
if err != nil {
fmt.Println("conn Write err=", err)
}
//fmt.Printf("客戶端發送了%d字節的數據,並退出",n)
}
}
Redis的使用
REmote Dictionary Server(遠程字典服務器),Redis性能非常高,單機能夠達到15w qps,通常適合做緩存,也可以持久化。是完全開源的,高性能的k-v分布式內存數據庫,基於內存運行並支持之久化的NoSQL數據庫。
Redis安裝好后,默認有16個數據庫,初始默認使用0號庫,編號0...15,select 1`切換1號數據庫。
golang操作redis
-
安裝第三方開源redis庫
cd $GOPATH go get github.com/garyburd/redigo/redis
-
Set/Get接口
func main() { //連接到redis conn,err := redis.Dial("tcp","127.0.0.1:6379") if err!= nil{ fmt.Println("redis.Dial err=",err) return } defer conn.Close() //通過go向redis寫入數據string[key-val] _,err = conn.Do("Set","name","tomjerry_cat") if err!= nil{ fmt.Println("set err=",err) return } //通過go向redis讀取數據 r,err:=redis.String(conn.Do("Get","name")) if err!= nil{ fmt.Println("get err=",err) return } //因為返回r是interface{},name對應的值是string,因此我們需要轉換 //nameString := r.(string) fmt.Println("操作ok",r) }
-
redis鏈接池
事先初始化一定數量的鏈接,放入到鏈接池,當 Go 需要操作 Redis 時,直接從 Redis 鏈接池取出鏈接即可,這樣可以節省臨時獲取 Redis 鏈接的時間,從而提高效率。
//定義一個全局的pool var pool *redis.Pool //當啟動程序時,就初始化鏈接池 func init() { pool = &redis.Pool{ MaxIdle: 8,//最大空閑鏈接數 MaxActive: 0,//表示和數據庫的最大鏈接數,0表示沒有限制 IdleTimeout: 100,//最大空閑時間 Dial: func() (redis.Conn, error) {//初始化鏈接代碼 return redis.Dial("tcp","localhost:6379") }, } } func main() { //先從pool取出一個鏈接 conn:=pool.Get() defer conn.Close() _,err:=conn.Do("Set","name","Tom cat!!") if err!=nil{ fmt.Println("conn.Do err=",err) return } //... }
經典項目——海量用戶即時通訊系統
海量用戶即時通訊系統,實現用戶登錄、注冊、顯示在線用戶列表、群聊等功