golang學習筆記(一)——golang基礎和相關資料結構

yuxiaoliang發表於2019-02-16

小白前端一枚,最近在研究golang,記錄自己學習過程中的一些筆記,以及自己的理解。

  • go中包的依賴管理
  • go中的切片
  • byte 和 string
  • go中的Map
  • go中的struct結構體
  • go中的方法
  • go中的interface介面
  • interface{}

原文在我的部落格中:https://github.com/forthealll…

歡迎star~

1、go中包的依賴管理

首先要了解的是GOPATH的含義,GOPATH是go命令依賴的重要變數,可以通過:

go env

來檢視相應的開發環境中GOPATH的值,也可以通過export GOPATH指定:

export GOPATH = /usr/local/go

指定GOPATH目錄後, GOPATH目錄包含了3個子目錄:

  • src存放原始碼(比如.go、.h檔案等)
  • pkg編譯時生成的中間檔案
  • bin編譯後生成的可執行檔案

此外,go的依賴管理中提供了3個主要的命令go build、go get和 go install。

  • go build: 會編譯目錄下或者指定的.go檔案,得到一個可執行的檔案
  • go install: 只能在GOPATH目錄下使用,與go build大致相同會編譯目錄下執行的.go檔案,此外go install還會將可執行檔案或庫檔案安裝到GOPATH目錄下。
  • go install + 遠端地址: 會將相應的程式碼下載到GOPATH同時會編譯該遠端包
  • go get + 遠端地址: 跟go install+遠端地址相同,會下載且編譯
  • go get u + 遠端地址: 下載並更新相應的遠端地址的包,但不會自動編譯

典型的例子,比如下載一個dep包:

go get -u github.com/golang/dep/cmd/dep

上述的go get和go install + 遠端包的方式,不能應用於需要版本管理依賴等場景,可以通過安裝dep包,來實現依賴管理。dep提供了幾個常用的命令,分別用於安裝和更新相應的go包。

  • dep init 初始化一個專案依賴
  • dep ensure 安裝專案所依賴的所有包
  • dep ensure -update 更新專案中的所有包
  • dep ensure -add github.com/pkg/errors 為專案新增單個依賴包

此外通過Gopkg.toml裡面可以指定所依賴包的git分支,版本號等等,且在dep ensure -add中也可以指定分支和版本號,比如:

dep ensure -add github.com/pkg/foo@^1.0.1

提到包(package),必須補充一句,在go中如果在其他包中引用變數,是通過:

包名.變數名

的形式,在這裡變數名必須是大寫的,也就是說在go的包中,變數能否匯出是根據變數的大小寫來確定的,普遍認為如果變數是大寫的就是在包內匯出的,如果是變數小寫的就是預設是包的私有變數。

2、go中的切片

在go的函式呼叫中,如果傳遞的引數是一個較大的陣列,顯然如果直接將陣列作為實參傳入,在執行函式的過程中,實際上會拷貝一份該陣列,會造成記憶體的浪費等。標準的做法,是傳入陣列的指標,或者對於陣列的部分引用。

這裡關於陣列的部分引用,就是slice切片

(1)、go中的切片簡介

陣列和切片之間存在著緊密的聯絡,slice提供了訪問陣列子序列的功能。所謂的切片是對於陣列的部分引用,slice由三部分組成指標、長度和容量。

  • 指標: 指向第一個slice元素所對應的陣列元素的地址
  • 長度: slice中元素的數目
  • 容量: slice中最多可容納元素的數目

切片的定義方式:

var slice1 []type = make([]type, len, cap)

分別指定切片的型別,長度以及容量。

切片的初始化:

s := [] int { 1,2,3 }

或者通過已經存在的陣列來實現切片的初始化,

arr = [10]int {1,2,3,4,5,6,7,8,9,10}
s:=arr[1:5] // arr[startIndex:endIndex]

(2)、go中的切片注意點

go中的slice切片有一個注意點,就是如何判斷切片為空,邊界情況大致如下所示:

var s []int   //len(s)==0,s==nil
s = nil       //len(s)==0,s==nil
s = []int(nil)//len(s)==0,s==nil
s = []int{}   //len(s)==0,s!=nil

顯然如果通過s==nil來判斷,不能區別第四種場景,因此判斷切片為空的正確方式是len(s)==0.

3、byte 和 string

下述的方法將返回一個byte的切片:

var test:= []byte("hello")

go遍歷slice動態刪除 map遍歷刪除安全.

4、go中的Map

map是一個無序的key/value對的集合,其中在每一個map中key是唯一的。go中的map只要坑在於map是無序的。

(1)、Map簡介

宣告一個map:

var ages map[string]int  //同樣初始的情況下,ages = nil
ages == nil // true

如果宣告瞭但是沒有賦值,那麼嘗試插入一對key/value會報錯,比如上述宣告但沒有初始化的情況下:

age["jony"] = 25 // 會panic

解決方法,就是給age定義後賦值:

ages = make(map[string]int)

或者定義的時候同時賦值:

ages := map[string]int{

}

此後插入不存在的key/value就不會報錯。

注意:嘗試從map中去一個不存在的key,預設的value值為0

(2)、Map無序性

我們從map的遍歷結果,來說明map是無序的。比如我們以這麼一個map為例:

var ages = map[string]int{
"a":21,
"b":22,
"c":23,
};
for name,age := range ages {
  fmt.Printf("%s	%d
",name,age);
}

通過for range可以遍歷map物件,分別執行三次遍歷後,來看遍歷的結果

  • 第一次輸出:

    c 23
    a 21
    b 22

  • 第二次輸出:
    c 23
    b 22
    a 21
  • 第三次輸出:
    a 21
    b 22
    c 23

從上述的結果我們也可以看出map的每次遍歷的結果都是不確定的。

注意:Map的value型別不僅僅可以是基本型別,也可以是聚合型別,比如map或者slice。

5 、go中的struct結構體

跟C++中的結構體類似,go中的結構體是一種聚合資料型別,由0個或者多個任意值聚合成實體。

(1)、結構體簡介

宣告一個結構體很簡單,比如我們宣告瞭一個Person結構體:

type Person struct {
   name string
   age int
   salary int
}

然後可以宣告一個Person型別的變數:

var person Person

然後可以通過點操作符訪問和賦值。

person.age = 25

此外,可以通過取地址符號加點操作符來訪問和賦值,下述取地址的方式效果與上述是相同的。

(&person).age = 25

此外,結構體也支援巢狀。

6、go中的方法

在go中沒有明確的定義類,但是可以將結構體struct來類比其他語言中的class。

go中的方法與結構體相關,為了說名go中的方法,我們先從go中的函式講起。

(1)、go中的函式簡介

在go中函式宣告包括函式名、形參列表、返回值列表(可省略 不傲視無返回值)以及函式體。

func name (parameter-list)(result-list){


}

比如我們有一個count函式可以如此簡單的定義:

func count(x,y int) int {
   return x + y
}

(2)、go中方法簡介

在函式定義的基礎上我們來介紹一下,如何定義方法。在函式宣告時,在函式名前放上一個變數,這個變數稱為方法的接收器,一般是結構體型別的。

當然也不一定是結構體,基本型別數值、字串、slice和map上面都可以作為接收器來定義方法。

宣告方法的方式具體可以如下所示:

func (receive Receive) name(parameter-list)(result-list){


}

從上述的宣告中也可以看出來只不過在函式的技術上增加了第一個引數接收器,為相應的接收器增加了該名稱的方法。比如我們定一個Person結構體,併為其宣告sellHello方法:

type Person struct {
   name string
   age int
   salary int
}

func (person Person) sayHello() string{
  return "Hello "+ person.name
}

p := Person{
   name: "Jony",
   age: 25,
   salary:100
}

fmt.Println(p.sayHello());//輸出Hello Jony

上述就是在結構體Person上定義了一個sayHello方法,在結構體被初始化後,可以通過p.sayHello()的方式直接呼叫。

除此之外,我們前面將到定義方法時的接收器不一定是一個結構體,接收器也可以接受基本型別等,比如:

type Mystring string;

func (mystring Mystring)sayHello() string{
  return "Hello"+ string(mystring);
}

var m Mystring
m = "Jony"
fmt.Println(m.sayHello());

上述的例子同樣會輸出Hello Jony.

甚至nil也可以作為方法的接收器,這裡就不具體舉例。

(3)、基於指標物件的方法

在函式呼叫時,是對實參的一個拷貝,如果函式需要更新一個變數,或者傳遞的引數過大,預設拷貝太為負責,我們經常會使用指標的形式,對於方法而言也同樣如此,也就是說方法的接收器可以是指標型別。

對比於上述非指標型別的方法,宣告指標型別的方法具體如下所示:

func (receive *Receive) name(parameter-list)(result-list){


}

指標型別的引數作為接收器,可以修改傳入引數的實際變數的值。

type Person struct {
  name string
  age int
  salary int
}
func (person *Person) changeAge(newAge int){
  (*person).age = newAge
}
p.changeAge(30);
fmt.Println(p.age); //輸出了30,發現age確實發生了改變。

7、go中的interface介面

我們前面也說過go不是一種傳統的物件導向的語言,沒有類和繼承的概念,go裡面通過interface介面可以實現很多物件導向的特性。

介面的通俗定義:

介面提供了一種方式來說明物件的行為,介面定義了一組方法,但是不包含實現。

(1)、interface介面簡介

可以通過如下格式來定義介面:

type Namer interface {
   Method1(param_list) return_type
   Method2(param_list) return_type
   ...
}

go中的介面都很簡短,一般包含了0-3個方法。

同時我們可以通過:

var ai Namer 

來定義一個介面型別的變數,初始值為nil.介面型別的變數是一個指標,宣告而未賦值的情況下就為nil。

go中的介面有以下需要注意的點:

  • 一個型別可以實現多個介面
  • 介面型別可以包含一個例項的引用,該例項的型別實現了此介面
  • 即使介面在型別之後定義,二者存在不同的包中,被單獨編譯,但是隻要型別實現了介面中的方法,它就實現了此介面
  • 實現某個介面的型別,除了實現介面方法外,還可以有其他的方法

上述幾點都比較好理解,具體第二點,舉例來說:

type Person struct {
 name string
 age int
 salary int
}
type Say interface {
  sayHello() string
}
func (person Person) sayHello() string {
  return "Hello "+person.name
}

func main() {
  p := new(Person)
  p.name = "Jony"

  var s Say;
  s = p;
  fmt.Println(s)
}

上述例子中,我們首先new了一個Person結構體型別的變數,並賦值給p,因為Person介面體中實現了Say介面中的所有方法sayHello等。因此我們就說Person實現了Say介面,因此Person的例項p,可以賦值給一個Say介面型別的變數s。

此時的s是個指標,指向Person結構體例項p。

(2)、interface介面型別斷言

任何型別只要實現了介面中的所有方法,我們就說該型別實現了該介面。這樣一個介面型別的變數varI可以包含任何型別的值,在go中提供了一種安全的方式來檢測它的動態型別。

if v,ok := varI.(T);ok {
   Process(v)
   return
}

如果轉化合法,那麼v是varI轉化到型別T的值,ok會是true,否則v是型別T的零值,ok是false。這是一種安全的轉化方式不會有錯誤發生。

我們還是接著上面的程式碼來講我們的例子:

type Person struct {
 name string
 age int
 salary int
}

type Say interface {
  sayHello() string
}

func (person Person) sayHello() string {
  return "Hello "+person.name
}

func main() {
  p := new(Person)
  p.name = "Jony"

  var s Say;
  s = p;
  if t,ok := s.(*Person);ok {
    fmt.Printf("The type of s is:%T
",t);
  }
}

輸出的結果為The type of s is:*main.Person。也可以使用特殊的type-switch來判斷。

switch t:= s.(*Person){
   case *Person:
      fmt.Printf("The type of s is:%T
",t);
   case nil:
      ...
   default:
      ...
}

8、interface{}

interface{}是一個空介面,任何型別的值都可以複製給interface{}型別的變數。

比如,我們首先宣告一個型別為interface{}的變數:

var test interface{}

任意型別的值都可以複製給test,比如下列基本型別的值複製給test是有效的:

var test interface{}
test = 1
test = true
test ="Hello"

此外,複雜的派生型別也可以賦值給test,我們以指標型別舉例:

var test interface{}
var a = 1
test = &a 

interface型別的變數是沒有型別的,但是我們可以人為的進行型別轉換:

var test interface{}
var a string
test = "hello"
a = test.(string)

上述,可以將test轉化成string型別,這樣就可以賦值給string型別變數a了。通過.(型別名)的方法可以將interface{}型別的變數轉化成任意的型別。

最後舉一個簡單的例子:

func main() {
   a := make([]interface{},10)
   b :=1
   a[1]=&b
   fmt.Println(*(a[1].(*int)))
}

上述程式碼發現,將interface{}型別切片中的某一元素的值複製給了int指標型別,然後進行了型別轉化,將interface{}型別的變數轉換成了int指標型別。

相關文章