後端語言很難?前端入門go基礎語法只需要3小時!(下)

語言: CN / TW / HK

繼續之前這兩篇文章,我們講到了介面。

介面

在 Go 中,介面是一種型別,它定義了一組方法。介面型別變數可以儲存任何實現了該介面的型別的值,這樣就可以在程式中使用統一的介面型別來處理不同的具體型別。

例如:

type Reader interface { Read(p []byte) (n int, err error) }

上面的程式碼定義了一個名為 Reader 的介面,該介面有一個方法 Read(p []byte) (n int, err error)。任何型別如果實現了Reader介面中的Read()方法,都可以被賦值給Reader型別變數.

介面還可以包含多個方法, 例如

type Writer interface { Write(p []byte) (n int, err error) Close() error }

這個Writer介面包含了兩個方法,Write和Close。任何型別如果實現了這兩個方法,就可以被賦值給Writer型別變數。

在 Go 中,介面是隱式實現的,因此實現介面不需要顯式宣告。只要型別定義了介面中的所有方法,就可以認為它實現了該介面。

介面還可以巢狀, 例如

type ReadWriter interface { Reader Writer }

這個ReadWriter介面包含了Reader和Writer兩個介面中所有的方法,任何型別實現了這兩個介面中所有的方法就可以被賦值給ReadWriter型別變數。

對比js:這個跟ts也差不多,但語法不太一樣,我們的interface要繼承或者type通過 && 來聚合,go還有點像ts中type這種組合的方式

介面的多型

Go 中,介面還可以用來實現多型。比如我們有一個函式,它接受一個介面型別的引數,那麼這個函式就可以接受任何實現了這個介面的型別的引數。這樣就可以在編譯時就發現型別問題,而不是在執行時。

例如:

``` type Shape interface { Area() float64 }

type Rectangle struct { width, height float64 }

func (r Rectangle) Area() float64 { return r.width * r.height }

type Circle struct { radius float64 }

func (c Circle) Area() float64 { return math.Pi * c.radius * c.radius }

func getArea(s Shape) float64 { return s.Area() }

func main() { r := Rectangle{width: 10, height: 5} c := Circle{radius: 5}

fmt.Println("Area of rectangle: ", getArea(r))
fmt.Println("Area of circle: ", getArea(c))

} ```

上面的程式碼定義了一個介面 Shape,它有一個方法 Area()。Rectangle 和 Circle 兩種型別都實現了這個方法,所以它們都實現了 Shape 介面。

這是go中實現面向物件繼承的內容,是很重要的,因為面向介面程式設計,是函式跟業務解耦具體業務是非常重要的,函式本身解耦了資料和行為,有了介面就把資料限制了,把資料變為一種協議,只要實現這個協議的資料就可以,從而實現面向介面實現函式

介面型別斷言和型別判斷

另外,Go 中的介面還支援型別斷言和型別判斷,可以在執行時判斷一個變數是否實現了某個介面。

型別斷言是指將介面型別斷言成具體型別。這樣就可以訪問具體型別的欄位和方法。型別斷言的語法如下:

x.(T)

x 是一個介面型別,T 是具體型別。如果 x 斷言成功,則返回 x 的具體值,否則返回一個型別斷言失敗的 panic。

型別判斷是指判斷介面型別是否實現了某個介面。這樣就可以在執行時判斷一個變數是否實現了某個介面。型別判斷的語法如下:

x.(type)

x 是一個介面型別。如果 x 實現了 T 介面,返回x的具體型別,否則返回nil

在 Go 中,型別斷言和型別判斷可以用來在執行時判斷一個變數是否實現了某個介面,並訪問具體的欄位和方法。這樣可以提高程式碼的靈活性。

我們舉例子說明一下:

型別斷言:

``` package main

import "fmt"

type animal interface { speak() }

type dog struct { }

func (d dog) speak() { fmt.Println("Woof!") }

func main() { var d animal = dog{} if val, ok := d.(dog); ok { val.speak() } else { fmt.Println("d is not a dog") } } ```

上面的程式碼中,我們定義了一個 animal 介面,並實現了一個 dog 型別。在 main 函式中,我們聲明瞭一個 animal 型別的變數 d,並將一個 dog 型別的變數賦值給它。然後我們使用型別斷言來判斷 d 是否是 dog 型別。如果斷言成功,就可以呼叫 dog 型別的 speak 方法。

注意:這裡我們需要注意的是,斷言是會返回內容的,這個跟typescript是完全不一樣的

型別判斷:

``` package main

import "fmt"

type animal interface { speak() }

type dog struct { }

func (d dog) speak() { fmt.Println("Woof!") }

func main() { var d animal = dog{} switch v := d.(type) { case dog: v.speak() default: fmt.Println("d is not a dog") } } ``` 我們定義了一個 animal 介面,並實現了一個 dog 型別。在 main 函式中,我們聲明瞭一個 animal 型別的變數 d,並將一個 dog 型別的變數賦值給它。然後我們使用型別判斷來判斷 d 是否實現了 dog 型別。如果判斷成功,就可以呼叫 dog 型別的 speak 方法。

總結:型別斷言和型別判斷都可以用來在執行時判斷一個變數是否實現了某個介面,並訪問具體的欄位和方法。但是型別斷言是訪問具體型別的值,型別判斷是訪問具體型別。

對比js,我們的ts好像沒有直接把型別變為字串的能力,畢竟js不是內建型別的,打通js和型別。

Goroutines和Channels

併發程式指同時進行多個任務的程式,隨著硬體的發展,併發程式變得越來越重要。Web伺服器會一次處理成千上萬的請求。平板電腦和手機app在渲染使用者畫面同時還會後臺執行各種計算任務和網路請求。即使是傳統的批處理問題--讀取資料,計算,寫輸出--現在也會用併發來隱藏掉I/O的操作延遲以充分利用現代計算機裝置的多個核心。計算機的效能每年都在以非線性的速度增長。

go的Goroutines可以類比以下協程,但是它們是有明顯區別。

在這裡很多前端同學對作業系統中,程序、執行緒、協程並不瞭解,我們有必要介紹一下:

一定注意,這些是大概念,具體到實現的語言裡,是有區別的 。

程序(Process),執行緒(Thread),[協程]

  • 程序:

一個程序是計算機中的一個獨立的程式關於某資料集合的一次執行活動,是系統進行資源分配和排程的基本單位。每個程序都有自己獨立的記憶體空間和系統資源,互不干擾。

程序一般由程式、資料集、程序控制塊三部分組成。

  • 程式: 指程序所要執行的指令集合,包括可執行程式的機器語言程式碼和資料。
  • 資料集: 指程序所需要的資料,包括全域性變數和區域性變數。
  • 程序控制塊: 是系統為每個程序維護的資料結構,記錄了程序的當前狀態,程序的基本資訊,如程序ID,優先順序,狀態,程序的資源資訊等。程序控制塊是系統維護程序資訊的重要資料結構,用於排程和管理程序,如記錄程序。

最後,程序的侷限是建立、撤銷和切換的開銷比較大。

  • 執行緒:

執行緒是程序的一個實體,是被系統獨立排程和分派的基本單位,執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程序的其他的執行緒共享程序所擁有的全部資源 執行緒執行緒是在程序之後發展出來的概念。

執行緒的優點是減小了程式併發執行時的開銷,提高了作業系統的併發效能,缺點是執行緒沒有自己的系統資源。

  • 協程:

協程: 協程是一種使用者態的輕量級執行緒, 它是程式設計師可控制的(使用者態執行),可以自行暫停和恢復執行,不由系統排程。

子程式呼叫總是一個入口,一次返回,一旦退出即完成了子程式的執行。

然後我們站在協程的角度看看它有什麼優缺點:

  • 優點:

  • 協程是輕量級的,它沒有執行緒那麼大的系統開銷,所以它比執行緒更容易建立和管理。

  • 協程是可控的,程式設計師可以自行控制協程的暫停和恢復,這樣可以更靈活的實現併發。
  • 協程能夠在單一執行緒中完成多工的排程,這樣可以減少執行緒上下文切換的開銷。

  • 缺點:

  • 協程需要額外的機制來避免資料衝突,這可能會增加程式的複雜性。

  • 協程不能利用多核處理器的優勢,因為它們執行在單一執行緒中。

go裡面的協程是共享資料的。但是,Go語言提供了一些機制來避免在多個協程之間共享變數時的資料衝突。

Go語言提供了一種叫做channel的機制,允許協程之間進行通訊。通過使用channel,可以在多個協程之間傳遞資料而不會發生資料衝突。

Go語言還提供了一種叫做互斥鎖(mutex)的機制,用於在多個協程之間同步訪問共享變數。使用互斥鎖可以保證在某一時刻只有一個協程能夠訪問共享變數。

Goroutines

在Go語言中,使用關鍵字go來啟動一個協程,如:

go foo()

這樣就會在單獨的協程中啟動函式foo()。

協程之間可以使用通道(channel)來進行通訊。通道是Go語言中的一種資料結構,可以用來在不同協程之間傳遞資料。

我們舉一個具體的例子:

一個實用的 Go 協程案例是網路爬蟲。網路爬蟲程式通常需要同時訪問多個網站,並在獲取資料後進行處理。使用協程可以在訪問一個網站時同時訪問其他網站,提高爬取效率。例如:

``` package main

import ( "fmt" "net/http" )

func main() { urls := []string{ "http://www.example.com", "http://www.example.net", "http://www.example.org", }

for _, url := range urls {
    go fetch(url)
}
fmt.Scanln()

}

func fetch(url string) { resp, err := http.Get(url) if err != nil { fmt.Println(err) return } defer resp.Body.Close() fmt.Println(url, resp.Status) } ```

在這個例子中,我們使用了 Go 內建的 net/http 包來訪問網站,並在迴圈中使用 go 關鍵字來併發地執行 fetch 函式。這樣,程式可以同時訪問多個網站,而不會阻塞在一個網站上。

Channels

Channels 是 Go 語言中的一種通訊機制,用於在 goroutines 之間進行同步和通訊。通過 Channels,一個 goroutine 可以將資料傳送到另一個 goroutine,並等待其接收。

Channels 類似於其他語言中的管道或佇列,但是 Channels 在 Go 中是一種內建型別,並提供了豐富的操作方法。

下面是一個網路爬蟲程式的例子,它使用了 Channels 來實現併發爬取,並在爬取完成後將資料傳送到另一個 goroutine 進行處理:

``` package main

import ( "fmt" "net/http" )

func main() { urls := []string{ "http://www.example.com", "http://www.example.net", "http://www.example.org", }

// 建立一個用於爬取的 channel
fetchChannel := make(chan string)

// 建立一個用於處理資料的 channel
processChannel := make(chan string)

// 啟動多個 goroutine 進行爬取
for _, url := range urls {
    go fetch(url, fetchChannel)
}

// 啟動一個 goroutine 來處理資料
go process(processChannel)

// 從 fetch channel 中讀取資料,併發送到 process channel
for i := 0; i < len(urls); i++ {
    fetchResult := <-fetchChannel
    processChannel <- fetchResult
}

close(processChannel)
fmt.Scanln()

}

func fetch(url string, fetchChannel chan string) { resp, err := http.Get(url) if err != nil { fetchChannel <- err.Error() return } defer resp.Body.Close() fetchChannel <- url + " " + resp.Status }

func process(processChannel chan string) { for data := range processChannel { fmt.Println(data) } } ``` 在上面的例子中,我們使用了兩個 channel

Go 語言中的包(package)是一種模組化程式設計的方式,用於將相關的型別、變數、函式和常量組織在一起。

所有的 Go 程式都必須在一個包中,main 包是一個特殊的包,它是程式的入口。

包中的型別、變數、函式和常量可以通過 import 關鍵字匯入到其他包中使用。

例如:

在一個名為 math 的包中定義了一個名為 Add 的函式,它接受兩個整型引數並返回它們的和。

``` package math

func Add(a, b int) int { return a + b } ```

在另一個名為 main 的包中,我們可以匯入 math 包並使用它的 Add 函式

``` package main

import "math"

func main() { result := math.Add(1, 2) fmt.Println(result) } ```

執行這個程式將會輸出 3。

Go 還支援匿名匯入,可以使用 _ 關鍵字匯入一個包,但不使用它的任何型別、變數、函式和常量。這可以用於匯入一個包中的 init 函式。

例如:

import _ "math/rand"

這樣會匯入 math/rand 包,但不會使用任何型別、變數、函式和常量。

匯入路徑

每個包是由一個全域性唯一的字串所標識的匯入路徑定位。出現在import語句中的匯入路徑也是字串。

import ( "fmt" "math/rand" "encoding/json" "golang.org/x/net/html" "github.com/go-sql-driver/mysql" )

如果你計劃分享或釋出包,那麼匯入路徑最好是全球唯一的。為了避免衝突,所有非標準庫包的匯入路徑建議以所在組織的網際網路域名為字首;而且這樣也有利於包的檢索。例如,上面的import語句匯入了Go團隊維護的HTML解析器和一個流行的第三方維護的MySQL驅動。

包宣告

在每個Go語音原始檔的開頭都必須有包宣告語句。包宣告語句的主要目的是確定當前包被其它包匯入時預設的識別符號(也稱為包名)。

例如,math/rand包的每個原始檔的開頭都包含package rand包宣告語句,所以當你匯入這個包,你就可以用rand.Int、rand.Float64類似的方式訪問包的成員。

``` package main

import ( "fmt" "math/rand" )

func main() { fmt.Println(rand.Int()) } ```

通常來說,預設的包名就是包匯入路徑名的最後一段,因此即使兩個包的匯入路徑不同,它們依然可能有一個相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。稍後我們將看到如何同時匯入兩個有相同包名的包。

關於預設包名一般採用匯入路徑名的最後一段的約定也有三種例外情況。

  • 第一種,包對應一個可執行程式,也就是main包,這時候main包本身的匯入路徑是無關緊要的。名字為main的包是給 go build 構建命令一個資訊,這個包編譯完之後必須呼叫聯結器生成一個可執行程式。

  • 第二種,包所在的目錄中可能有一些檔名是以*test.go為字尾的Go原始檔。並且這些原始檔宣告的包名也是以_test為字尾名的。這種目錄可以包含兩種包:

    • 一種普通包,
    • 一種則是測試的外部擴充套件包。

所有以_test為字尾包名的測試外部擴充套件包都由go test命令獨立編譯,普通包和測試的外部擴充套件包是相互獨立的。後面會介紹test的內容

  • 第三種,一些依賴版本號的管理工具會在匯入路徑後追加版本號資訊,例如"gopkg.in/yaml.v2"。這種情況下包的名字並不包含版本號字尾,而是yaml。"

測試

Maurice Wilkes,第一個儲存程式計算機EDSAC的設計者,1949年他在實驗室爬樓梯時有一個頓悟。在《計算機先驅回憶錄》(Memoirs of a Computer Pioneer)裡,他回憶到:“忽然間有一種醍醐灌頂的感覺,我整個後半生的美好時光都將在尋找程式BUG中度過了”。肯定從那之後的大部分正常的碼農都會同情Wilkes過份悲觀的想法,雖然也許不是沒有人困惑於他對軟體開發的難度的天真看法。

現在的程式已經遠比Wilkes時代的更大也更復雜,也有許多技術可以讓軟體的複雜性可得到控制。其中有兩種技術在實踐中證明是比較有效的。第一種是程式碼在被正式部署前需要進行程式碼評審。第二種則是測試,也就是本章的討論主題。

測試函式

每個測試函式必須匯入testing包。測試函式有如下的簽名: ``` func TestName(t *testing.T) {

// ...

} 測試函式的名字必須以Test開頭,可選的字尾名必須以大寫字母開頭: func TestSin(t testing.T) { / ... */ }

func TestCos(t testing.T) { / ... */ }

func TestLog(t testing.T) { / ... */ } ```

其中t引數用於報告測試失敗和附加的日誌資訊。

我們來舉一個例子:

gopl.io/ch11/word1 ``` // Package word provides utilities for word games. package word

// IsPalindrome reports whether s reads the same forward and backward. // (Our first attempt.) func IsPalindrome(s string) bool { for i := range s { if s[i] != s[len(s)-1-i] { return false } } return true } ```

在相同的目錄下,word_test.go測試檔案中包含了TestPalindrome和TestNonPalindrome兩個測試函式。每一個都是測試IsPalindrome是否給出正確的結果,並使用t.Error報告失敗資訊:

``` package word

import "testing"

func TestPalindrome(t *testing.T) { if !IsPalindrome("detartrated") { t.Error(IsPalindrome("detartrated") = false) } if !IsPalindrome("kayak") { t.Error(IsPalindrome("kayak") = false) } }

func TestNonPalindrome(t *testing.T) { if IsPalindrome("palindrome") { t.Error(IsPalindrome("palindrome") = true) } } `go test`命令如果沒有引數指定包那麼將預設採用當前目錄對應的包(和`go build`命令一樣)。我們可以用下面的命令構建和執行測試。 $ cd $GOPATH/src/gopl.io/ch11/word1

$ go test

ok gopl.io/ch11/word1 0.008s ```

  • 測試用例名稱一般命名為 Test 加上待測試的方法名。
  • 測試用的引數有且只有一個,在這裡是 t *testing.T
  • 基準測試(benchmark)的引數是 *testing.B,TestMain 的引數是 *testing.M 型別。

go test -v-v 引數會顯示每個用例的測試結果,另外 -cover 引數可以檢視覆蓋率

例如下面的:

bash $ go test -v === RUN TestAdd --- PASS: TestAdd (0.00s) === RUN TestMul --- PASS: TestMul (0.00s) PASS ok example 0.007s

如果只想執行其中的一個用例,例如 TestAdd,可以用 -run 引數指定,該引數支援萬用字元 *,和部分正則表示式,例如 ^$

bash $ go test -run TestAdd -v === RUN TestAdd --- PASS: TestAdd (0.00s) PASS ok example 0.007s

子測試(Subtests)

子測試是 Go 語言內建支援的,可以在某個測試用例中,根據測試場景使用 t.Run建立不同的子測試用例:

``` // calc_test.go

func TestMul(t testing.T) { t.Run("pos", func(t testing.T) { if Mul(2, 3) != 6 { t.Fatal("fail") }

})
t.Run("neg", func(t *testing.T) {
    if Mul(2, -3) != -6 {
        t.Fatal("fail")
    }
})

} ```

  • 之前的例子測試失敗時使用 t.Error/t.Errorf,這個例子中使用 t.Fatal/t.Fatalf,區別在於前者遇錯不停,還會繼續執行其他的測試用例,後者遇錯即停。

反射

反射是指在執行時動態獲取和操作型別、變數、函式和介面的能力。在 Go 語言中,反射是通過內建的 reflect 包實現的。

常用的api如下:

reflect.ValueOf(x) 會返回一個 reflect.Value 型別的變數,它包含了變數的值和型別資訊。通過這個變數,我們可以獲取變數的值,修改變數的值,獲取變數的型別和類別等。

reflect.TypeOf(x) 會返回一個 reflect.Type 型別的變數,它包含了變數的型別資訊。通過這個變數,我們可以獲取變數的型別名稱,獲取欄位和方法等。

舉個例子:

``` package main

import ( "fmt" "reflect" )

func main() { var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float()) }

``` 這裡我們使用反射獲取了一個 float64 型別的變數的型別、類別和值。 輸出:

type: float64 kind is float64: true value: 3.4 我們可以在執行時動態獲取變數的型別,類別和值,並進行各種操作。

在 Go 中,反射還可以用來獲取結構體的欄位、方法和標籤。

舉個例子:

``` package main

import ( "fmt" "reflect" )

type Person struct { Name string json:"name" Age int json:"age" }

func (p Person) SayHello() { fmt.Println("Hello, my name is", p.Name) }

func main() { p := Person{Name: "John", Age: 30} t := reflect.TypeOf(p)

// 獲取欄位
fmt.Println("fields:")
for i := 0; i < t.NumField(); i++ {
    f := t.Field(i)
    fmt.Printf("%d: %s %s json=%s\n", i, f.Name, f.Type, f.Tag.Get("json"))
}

// 獲取方法
fmt.Println("\nmethods:")
for i := 0; i < t.NumMethod(); i++ {
    m := t.Method(i)
    fmt.Printf("%d: %s\n", i, m.Name)
}

} ```

輸出:

``` fields: 0: Name string json=name 1: Age int json=age

methods: 0: SayHello ```

我們通過反射獲取了結構體 Person 的欄位名稱、型別和標籤,以及方法名稱。

最基礎的部分已經完結

接下來,是看了一個視訊(),總結了一些語法上的常見坑點。

型別轉化

與ts的區別:

1、Go語言不允許隱式型別轉換 2、別名和原有型別也不能進行隱式型別轉換

``` package main

func main(){ var a intt = 1 var b int64 b = a // 報錯 } ```

如何修正,顯示型別轉換即可 b = int64(a)

我們再看下別名

``` package main

type MyInt int64

func main(){ var b int64 var c MyInt c = b // 報錯 修正的話:c = MyInt(b) } ```

指標型別

  • 不支援指標運算
  • 與ts的區別: string是值型別,預設的初始化值是空字串,不是undefined

``` package main

type MyInt int64

func main(){ a := 1 aPoint := &a aPoint := aPoint + 1 var b string // 被初始化為一個空值 } ```

算數運算子

  • go沒有前置++

while迴圈

與js的差別,沒有while迴圈,但可以用for迴圈來實現

n := 0 for n < 5 { n++ fmt.Println(n) }

if語句

怎麼說呢,if的go風格是下面這樣的,跟js不太像,跟node其實有點像,node以前是回撥函式都有錯誤優先,go也是這樣的,只不過寫起來語法不一樣,思想是一樣的。 ``` package main

func main(){ if v, err := someFun(); err == nil { xxx } else { xxx } } ```

switch語句也有類似的用法

switch os:= runtime.GOOS; os { case "darwin": fmt.Println("OS X.") case "linux" fmt.Println("Linux.") default: fmt.Printf("%s.", os) }

switch還有一種用法,跟js完全不同,下面的逗號相當於匹配任意一個就算true

switch i { case 0, 2: xx case 1, 3: xx default: xx }

多維陣列

例如如何在go中宣告2維陣列

c := [2][2]int{{1, 2}, {3, 4}}

go中判斷map中某個key是否元素存在

寫法如下,跟js大不相同

``` m1 := map[int]int{} m1[2] = 0

if v,ok := m1[3]; ok {

} else {

} ```

go中的slice在什麼情況下會共享資料

在 Go 中,slice 是對陣列的一個封裝。當一個新的 slice 是由一個已經存在的 slice 建立時,兩個 slice 會共享同一個底層陣列。

這可能會發生在以下兩種情況:

  1. 通過切片語法建立新的 slice:

original := []int{1, 2, 3, 4, 5} // Create a new slice that shares the same underlying array new := original[1:3]

  1. 通過呼叫內建函式 make 建立新的 slice,並且指定了第三個引數,並且這個引數不為0,表示容量而不是長度,這樣建立的slice 會共享同一個底層陣列:

original := make([]int, 5, 10) // Create a new slice that shares the same underlying array new := original[:3]

在這些情況下,原始的 slice 和新的 slice 都共享相同的底層陣列。當修改其中一個 slice 中的資料時,另一個 slice 中的資料也會被更改。

當然在建立新的slice的時候如果是通過append()來建立的就不會共享資料了,因為append會新開一塊新的記憶體來儲存資料。

go裡面的相面物件其實沒有嚴格的繼承功能

網上很多文章說go支援繼承,其實並不支援,嚴格來講應該算是組合,而不是繼承,類似設計模式的橋接模式。

在Go語言中,可以通過組合一個型別的結構體欄位來實現類似繼承的功能。這種方式更加簡潔、清晰,同時也避免了繼承所帶來的複雜性和靈活性問題。

go中的字串表現為Unicode字元組成的byte切片

在 Go 語言中,字串是由一系列Unicode字元組成的,字串在記憶體中是以UTF-8編碼的形式儲存的。實際上,字串在記憶體中是一個byte切片,每個字元在記憶體裡是一段連續的區間。

空介面可以表示任何型別

在 Go 中,空介面(interface{})表示可以儲存任何型別的值。因為所有型別都滿足空介面的約束(不需要實現任何方法),所以可以將任何型別的值賦值給空介面型別的變數。這使得空介面非常適用於實現通用函式、資料結構等。

注意,可以通過斷言來將空介面轉換為指定型別

v, ok := p.(int) // ok等於true時,轉換成功

go介面的最佳實踐

傾向於使用小的介面定義,很多介面只包含一個方法,比如

type Reader interface { Read(p []byte)(n int, err error) } type Writer interface { Write(p []byte)(n int, err error) } 較大的介面定義,可以由多個小介面定義組合而成 type ReadWriter interface { Reader Writer }

只依賴於必要功能的最小介面

func StoreData(reader Reader) error { ... }

GO沒有錯誤機制

比如沒有try catch語句。我們可以通過errors.New來快速建立錯誤例項,利用多返回值的特性,去處理錯誤

errors.New("n must be int rhe range [0, 1]")

錯誤處理原則

及早失敗,避免巢狀

就是說如果函式報錯,就直接return了

錯誤的recover

常常有同學這樣寫程式碼:

defer func() { if err := recover(); err != nil { log.Error("recover panic", err) } }

因為錯誤可能是我們系統中某些資源消耗完了,我這樣恢復了,其實系統依然是不能工作的,我們的健康檢查也很難去檢查出現在的問題,因為常常健康檢查只是檢測當前的應用是否正常提供服務,結果還是在的,結果你已經不能提供正常的服務了。

如何處理呢?

"Let it Crash",重啟大法好啊!

package一致

同一目錄裡的Go程式碼的package要保持一致,要不編譯不通過

init可以定義多個

首先 init函式時在main函式被執行前,所有依賴的package的init方法都會被執行

並且包的每個原始檔也可以有多個init函式,這點比較特殊

例如:

// my_series.go

``` package series

func init() { fmt.Println("init1") }

func init() { fmt.Println("init2") }

func Square(n int) int { return n * n } ```

``` package main

import "ch15/series"

func main() { fmt.Println(series.Square(2)) } ```

最後輸出,init的函式依次執行了

go get 命令預設訪問https

"go get -u" 是 Go 語言中的命令,用於安裝和更新 Go 包。

"go get" 命令用於安裝 Go 包,它會從遠端程式碼庫下載包的原始碼,並安裝到本地。

"-u" 引數表示更新,當安裝已經存在的包時,會更新這個包到最新版本。

例如,執行 "go get -u github.com/golang/example" 命令,會安裝或更新名為 "example" 的包,並將其安裝到 $GOPATH/src/github.com/golang 目錄下。

需要注意的是,go get命令預設會訪問https的程式碼庫,如果你需要訪問http的程式碼庫或者本地目錄,需要使用-d引數。

協程的原理

go的協程開銷非常小,初始化的棧只有2k,而java的執行緒棧大小是1M。java執行緒和核心物件的對映是1:1,而go的協程和核心物件的對映是M:N(多對多),明顯go要更強。

協程併發處理的機制:

協程.png

上圖中,M代表的是系統執行緒,P是GO語言自己實現的協程處理器,G是一個協程任務,組成協程佇列。

如果一個協程花費的時間特別長

機制1:協程的守護程序會記錄協程處理器完成的協程數量,如果一段時間內,處理器完成的數量沒有變化,就會往任務棧中插入一個標記,當前協程遇到非行內函數時,在任務棧中讀到該標記,就會把該協程任務移到佇列的末尾;

機制2:如果執行協程過程中遇到一個非CPU密集型任務,例如IO,需要中斷等待時,協程處理器P會把自己移到另外一個可使用的系統執行緒中,繼續執行佇列中其他的協程任務。當中斷的協程被重新喚醒後,會把自己重新加入到協程佇列裡或者全域性等待佇列中。其中,中斷的協程會把context儲存在協程物件裡,當協程被喚醒後會將其重新讀取到暫存器中,繼續執行。

一個很容出錯的案例:

func TestGroutine(t *testing.T) { for i := 0; i < 10; i++ { go func(i int) { fmt.Println(i) }() } } 其中i列印的值都是10,因為go 後面的程式碼有點像js的非同步,for迴圈是同步程式碼,當迴圈完畢,i已經是10了,然後因為go又是詞法作用域,所以,每次去尋找外部i的值,都是10。

參考: - https://geektutu.com/post/quick-go-test.html