Golang開發必須了解的細節!


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
    	}
    	
    	//...
    }
    

經典項目——海量用戶即時通訊系統

海量用戶即時通訊系統,實現用戶登錄、注冊、顯示在線用戶列表、群聊等功


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM