golang快速入門


1.打包和工具鏈

1.1 包

所有 Go 語言的程序都會組織成若干組文件,每組文件被稱為一個包。
net/http/
    cgi/
    cookiejar/
        testdata/
    fcgi/
    httptest/
    httputil/
    pprof/
    testdata/
在 http 目錄下的所有文件都屬於 http 包
所有的.go 文件,除了空行和注釋,都應該在第一行聲明自己所屬的包。每個包都在一個單 獨的目錄里。不能把多個包放到同一個目錄中,也不能把同一個包的文件分拆到多個不同目錄中。 這意味着,同一個目錄下的所有.go 文件必須聲明同一個包名。
 
在 Go 語言里,命名為 main 的包具有特殊的含義。 所有用 Go 語言編譯的可執行程序都必須有一個名叫 main 的包。

1.2 導入

import (
    "fmt"
    "strings")
編譯器先查找$GOROOT下的包,然后查找$GOPATH下的包
1.推薦所有自定義包不放在$GOROOT下,該目錄下一般是標准庫,自定義包放在$GOPATH下,如果升級go版本,直接替換/usr/local/go相關文件,而不用重新替換自定義包
2.go語言中字符串使用雙引號"",注意與Python習慣區別

go支持遠程導入,如果import內的包名是一個地址,如"github.com/spf13/viper",在go run之前使用go get命令,程序會下載對應的包的$GOPATH的包目錄下(該功能需要git支持)

命名導入和未使用包標注
import {
    myfmt "mylib/fmt"    //重命名包名,在有多個包重名的情況下使用,同Python中的import package as pkg
    _ "fmt"   //go語言不允許導入包而不使用,所以用下划線標注沒有使用的包,實際不會導入
}

1.3 go工具介紹

  • go build file.go         //編譯文件
  • go clean file.go        //刪除編譯生成的可執行文件
  • go vet file.go           //檢查常見錯誤Printf類型匹配錯誤的參數,定義函數時方法簽名錯誤,錯誤結構變量等
  • go fmt file.go        //自動整理文件格式,對齊
  • go doc pkg              //在終端查看包相關的文檔
  • godoc -http=:80     //啟動一個go文檔web服務器,如果開發人員按照godoc規則寫代碼,能自動包含在文檔中

2.數組、切片和映射

2.1 數組
// 聲明一個包含5個元素的整型數組
// 一旦聲明,數組里存儲的數據類型和數組長度就都不能改變
var array [5]int                //數組默認值為0
array := [5]int{10, 20, 30, 40, 50}
array := [...]int{10, 20, 30, 40, 50}
array := [5]int{1: 10, 2: 20}                  //指定索引為1元素為10,索引為2元素為20

// 聲明包含 5 個元素的指向整數的數組
// 用整型指針初始化索引為 0 和 1 的數組元素
array := [5]*int{0: new(int), 1: new(int)}
*array[0] = 10    // 為索引為0的元素賦值

var array_ [5]int
array_ = array
// 數組變量的類型包括數組長度和每個元素的類型。只有這兩部分都相同的數組,才是類型相 同的數組,才能互相賦值

// 聲明一個二維整型數組,兩個維度分別存儲 4 個元素和 2 個元素
var array [4][2]int

// 使用數組字面量來聲明並初始化一個二維整型數組
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}

// 聲明並初始化外層數組中索引為 1 個和 3 的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}

// 聲明並初始化外層數組和內層數組的單個元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}

// 聲明一個需要 8 MB 的數組
var array [1e6]int
// 將數組傳遞給函數
foo foo(array)
//函數 foo 接受一個 100 萬個整型值的數組
func foo(array [1e6]int)
{ ... }
每次函數 foo 被調用時,必須在棧上分配 8 MB 的內存。之后,整個數組的值(8 MB 的內 存)被復制到剛分配的內存里。雖然 Go 語言自己會處理這個復制操作,不過還有一種更好且更 有效的方法來處理這個操作。可以只傳入指向數組的指針,這樣只需要復制 8 字節的數據而不是 8 MB 的內存數據到棧上,優化如下
// 分配一個需要 8 MB 的數組
var array [1e6]int
// 將數組的地址傳遞給函數
foo foo(&array)
// 函數 foo 接受一個指向 100 萬個整型值的數組的指針
func foo(array *[1e6]int)
{ ... }
這個操作會更有效地利用內存,性能也更好。不過要意識到,因為現在傳遞的是指針, 所以如果改變指針指向的值,會改變共享的內存。使用切片能更好地處理這類共 享問題。

2.2 切片

切片是一個很小的對象,對底層數組進行了抽象,並提供相關的操作方法
切片有三個字段的數據結構:指向底層數組的指針、切片訪問的元素的個數(即長度)和切片允許增長 到的元素個數(即容量)
// 創建一個字符串切片
// 其長度和容量都是 5 個元素
slice := make([]string, 5)

// 創建一個整型切片
// 其長度為 3 個元素,容量為 5 個元素,容量小於長度的切片會編譯出錯
slice := make([]int, 3, 5)
slice := []int{10, 20, 30}
// 使用空字符串初始化第 100 個元素
slice := []string{99: ""}

// 創建 nil 整型切片
var slice []int
slice := make([]int, 0)
slice := []int{}

如果在[]運算符里指定了一個值,那么創建的就是數組而不是切片。只有不指定值 的時候,才會創建切片

切片的使用

// 其長度和容量都是 5 個元素
slice := []int{10, 20, 30, 40, 50}

// 其長度為 2 個元素,容量為 4 個元素,該切片不能看見底層數組第0號元素
newSlice := slice[1:3]

// 使用原有的容量來分配一個新元素
// 將新元素賦值為 60
newSlice = append(newSlice, 60)

因為 newSlice 在底層數組里還有額外的容量可用,append 操作將可用的元素合並到切片 的長度,並對其進行賦值。由於和原始的 slice 共享同一個底層數組,slice 中索引為 3 的元 素的值也被改動了

如果切片的底層數組沒有足夠的可用容量,append 函數會創建一個新的底層數組,將被引用的現有的值復制到新數組里,再追加新的值

綜上:創建切片的時候盡量使長度和容量一致,如果增加append新值是新開一個底層數組,而不是直接修改

e.g:
    source := []int{0, 1, 2, 3}
    slice1 := source[0:3]
    fmt.Println(slice1)
    // [0 1 2]
    slice2 := append(slice1, 100)
    slice2[0] = 99
    fmt.Println(slice1)
    fmt.Println(slice2)
    // [99 1 2]
    // [99 1 2 100]

迭代切片
slice := []int{10, 20, 30, 40}
// 迭代每一個元素,並顯示其值
for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}
如果需要忽略index值,使用下划線占位
當迭代切片時,關鍵字 range 會返回兩個值。第一個值是當前迭代到的索引位置,第二個 值是該位置對應元素值的一份副本
slice := []int{10, 20, 30, 40}
for index, value := range slice {
    // 輸出值和地址
    fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
        value, &value, &slice[index])
}

//第二種迭代方式
for index := 0; index < len(slice); index++ {
    fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
關鍵字 range 總是會從切片頭部開始迭代。如果想對迭代做更多的控制,依舊可以使用傳 統的 for 循環

多維切片

// 創建一個整型切片的切片
slice := [][]int{{10}, {100, 200}}
// 為第一個切片追加值為 20 的元素
slice[0] = append(slice[0], 20

切片屬於引用類型,在函數間傳遞開銷很小

舉例理解切片和底層數組

// 長度為3, 容量為5
slice1 := make([]int, 3, 5)   // 修改5為3,創建一個長度與容量一致的切片
// 切片所有默認值都是0
slice1[1] = 1
slice1[2] = 2
// 切片2與1共享一個底層數組
slice2 := slice1[0:3]
// 在切片2上面增加一個數據
slice3 := append(slice2, 200)
// 在切片1上面增加一個數據
slice4 := append(slice1, 100)
fmt.Println(slice1) // [0, 1, 2]
fmt.Println(slice2) // [0, 1, 2]
fmt.Println(slice3) // [0, 1, 2, 100]
fmt.Println(slice4) // [0, 1, 2, 100]
再次說明: 內置函數 append 會首先使用可用容量。一旦沒有可用容量,會分配一個 新的底層數組。這導致很容易忘記切片間正在共享同一個底層數組。一旦發生這種情況,對切片 進行修改,很可能會導致隨機且奇怪的問題。對切片內容的修改會影響多個切片,卻很難找到問 題的原因。
2.3 映射

映射是一個集合,可以使用類似處理數組和切片的方式迭代映射中的元素。但映射是無序的 集合,意味着沒有辦法預測鍵值對被返回的順序。即便使用同樣的順序保存鍵值對,每次迭代映 射的時候順序也可能不一樣。

// 創建一個映射,鍵的類型是 string,值的類型是
int dict := make(map[string]int)
// 創建一個映射,鍵和值的類型都是 string
// 使用兩個鍵值對初始化映射
dict := map[string]string{"a": "1", "b": "2"}
// 重新賦值
dict["a"]="3"
// 刪除
delete(dict, "a")

// 判斷key是否存在
value, exists := dict["a"]
if exists{
    fmt.Println(value)
}

// 使用ranged迭代
for key, value := range dict{
    fmt.Printf("Key: %s Value: %s\n", key, value)
}

映射的鍵可以是任何值。這個值的類型可以是內置的類型,也可以是結構類型,只要這個值 可以使用==運算符做比較。
切片、函數以及包含切片的結構類型這些類型由於具有引用語義, 不能作為映射的鍵,使用這些類型會造成編譯錯誤
e.g1:
dict := map[[]string]int{}
fmt.Println(dict)
// # command-line-arguments
// ./main.go:9:10: invalid map key type []string

dict := map[[3]string]int{}
// map[]

e.g2:
func main() {
    // 創建一個映射, 字母與對應的10進制ascii碼
    dict := map[string]int{"a": 97, "b": 98, "c": 99}
    for key, value := range dict {
        fmt.Printf("key: %s Value: %s\n", key, value)
    }
    removeDict(dict, "b")
    fmt.Println(dict)
}
func removeDict(OneDict map[string]int, key string) {
    delete(OneDict, key)
}
1.Print以ln結尾的是直接輸出,類似Python的print,可以輸出各種類型(包括自定義結構體)變量並換行,一行輸出多個值使用逗號隔開;以f結尾的是結構化輸出,類似c語言中的printf
2.在函數間傳遞映射並不會制造出該映射的一個副本。實際上,當傳遞映射給一個函數,並對 這個映射做了修改時,所有對這個映射的引用都會察覺到這個修改。這個特性和切片類似,保證可以用很小的成本來復制映射

3.GO語言的類型系統

// user 在程序里定義一個用戶類型
// 屬性的類型也可以是用戶自定義類型,用法類似c語言結構體struct
type user struct {
    name       string
    email      string
    ext        int
    privileged bool
}
// 聲明 user 類型的變量
var bill user
// 聲明變量並賦值
lisa := user{
        name:       "Lisa",
        email:      "lisa@email.com",
        ext:        123,
        privileged: true,}
// 順序必須與聲明一致
lisa := user{"Lisa", "lisa@email.com", 123, true}

// 聲明一個新類型
type Duration int64
// int64 類型叫作 Duration 的基礎類型。不過,雖然 int64 是基礎 類型,Go 並不認為 Duration 和 int64 是同一種類型。這兩個類型是完全不同的有區別的 類型。

e.g1:
var dur Duration
dur = int64(1000)
./main.go:12:6: cannot use int64(1000) (type int64) as type Duration in assignment

e.g2:

// 這個示例程序展示如何聲明,並使用方法
package main
import (
    "fmt"
)
// user 在程序里定義一個用戶類型
type user struct {
    name  string
    email string
}
// notify 使用值接收者實現了一個方法
func (u user) notify() {
    fmt.Printf("Sending User Email To %s<%s>\n",
        u.name,
        u.email)
}
// changeEmail 使用指針接收者實現了一個方法
// 這個方法使用指針接收者聲明。這個接收者的類型是指向 user 類型值的指針,而不是 user 類型的值。當調用使用指針接收者聲明的方法時,這個方法會共享調用方法時接收者所指向的值
func (u *user) changeEmail(email string) {
    u.email = email
}
// main 是應用程序的入口
func main() {
    // user 類型的值可以用來調用
    // 使用值接收者聲明的方法
    bill := user{"Bill", "bill@email.com"}
    bill.notify()
    // 指向 user 類型值的指針也可以用來調用
    // 使用值接收者聲明的方法
    lisa := &user{"Lisa", "lisa@email.com"}
    lisa.notify()
    //指針被解引用為值, 這樣就符合了值接收者的要求。notify 操作的是一個副本,只不過這次操作的是 從 lisa 指針指向的值的副本。
    // (*lisa).notify()

    // user 類型的值可以用來調用
    // 使用指針接收者聲明的方法
    bill.changeEmail("bill@newdomain.com")
    bill.notify()
    //變量 bill,以及之后使用這個變量調用使用指針接收者聲明的 changeEmail 方法。Go 語言再一次對值做了調整,使之符合函數的接收者,進行調用
    // (&bill).changeEmail ("bill@newdomain.com")

    // 指向 user 類型值的指針可以用來調用
    // 使用指針接收者聲明的方法
    lisa.changeEmail("lisa@newdomain.com")
    lisa.notify()
}
// Go 語言既允許使用值,也允許使用指針來調用方法,不必嚴格符合接收者的類型。

  


免責聲明!

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



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