1. 程式人生 > 其它 >GO語言基礎教程

GO語言基礎教程

技術標籤:golanggolang程式語言

GO語言基礎教程

文章目錄

1、Hello World

1.1 初體驗

main.go

package main

import "fmt"

func main() {
	fmt.Print("Hello World")
}

執行 go run main.go

編譯程式命令 為go build

1.2 fmt格式化引數介紹

原始碼介紹路徑

C:\Go\src\fmt\doc.go

中文文件參考 https://studygolang.com/pkgdoc fmt篇

通用:

%v	值的預設格式表示
%+v	類似%v,但輸出結構體時會新增欄位名
%#v	值的Go語法表示
%T	值的型別的Go語法表示
%%	百分號

布林值:

%t	單詞true或false

整數:

%b	表示為二進位制
%c	該值對應的unicode碼值
%d	表示為十進位制
%o	表示為八進位制
%q	該值對應的單引號括起來的go語法字元字面值,必要時會採用安全的轉義表示
%x	表示為十六進位制,使用a-f
%X	表示為十六進位制,使用A-F
%U	表示為Unicode格式:U+1234,等價於"U+%04X"

浮點數與複數的兩個組分:

%b	無小數部分、二進位制指數的科學計數法,如-123456p-78;參見strconv.FormatFloat
%e	科學計數法,如-1234.456e+78
%E	科學計數法,如-1234.456E+78
%f	有小數部分但無指數部分,如123.456
%F	等價於%f
%g	根據實際情況採用%e或%f格式(以獲得更簡潔、準確的輸出)
%G	根據實際情況採用%E或%F格式(以獲得更簡潔、準確的輸出)

字串和[]byte:

%s	直接輸出字串或者[]byte
%q	該值對應的雙引號括起來的go語法字串字面值,必要時會採用安全的轉義表示
%x	每個位元組用兩字元十六進位制數表示(使用a-f)
%X	每個位元組用兩字元十六進位制數表示(使用A-F)    

指標:

%p	表示為十六進位制,並加上前導的0x    

沒有%u。整數如果是無符號型別自然輸出也是無符號的。類似的,也沒有必要指定運算元的尺寸(int8,int64)。

寬度通過一個緊跟在百分號後面的十進位制數指定,如果未指定寬度,則表示值時除必需之外不作填充。精度通過(可選的)寬度後跟點號後跟的十進位制數指定。如果未指定精度,會使用預設精度;如果點號後沒有跟數字,表示精度為0。舉例如下:

%f:    預設寬度,預設精度
%9f    寬度9,預設精度
%.2f   預設寬度,精度2
%9.2f  寬度9,精度2
%9.f   寬度9,精度0    

1.3 fmt例子

一定要記住的

%v %+v %#v %T %s %d %p

//https://blog.csdn.net/qq_34777600/article/details/81266453
package main
import "fmt"
import "os"
type point struct {
    x, y int
}
func main() {
    //Go 為常規 Go 值的格式化設計提供了多種列印方式。例如,這裡列印了 point 結構體的一個例項。
    p := point{1, 2}
    fmt.Printf("%v\n", p) // {1 2}
    //如果值是一個結構體,%+v 的格式化輸出內容將包括結構體的欄位名。
    fmt.Printf("%+v\n", p) // {x:1 y:2}
    //%#v 形式則輸出這個值的 Go 語法表示。例如,值的執行原始碼片段。
    fmt.Printf("%#v\n", p) // main.point{x:1, y:2}
    //需要列印值的型別,使用 %T。
    fmt.Printf("%T\n", p) // main.point
    //格式化布林值是簡單的。
    fmt.Printf("%t\n", true)
    //格式化整形數有多種方式,使用 %d進行標準的十進位制格式化。
    fmt.Printf("%d\n", 123)
    //這個輸出二進位制表示形式。
    fmt.Printf("%b\n", 14)
    //這個輸出給定整數的對應字元。
    fmt.Printf("%c\n", 33)
    //%x 提供十六進位制編碼。
    fmt.Printf("%x\n", 456)
    //對於浮點型同樣有很多的格式化選項。使用 %f 進行最基本的十進位制格式化。
    fmt.Printf("%f\n", 78.9)
    //%e 和 %E 將浮點型格式化為(稍微有一點不同的)科學技科學記數法表示形式。
    fmt.Printf("%e\n", 123400000.0)
    fmt.Printf("%E\n", 123400000.0)
    //使用 %s 進行基本的字串輸出。
    fmt.Printf("%s\n", "\"string\"")
    //像 Go 原始碼中那樣帶有雙引號的輸出,使用 %q。
    fmt.Printf("%q\n", "\"string\"")
    //和上面的整形數一樣,%x 輸出使用 base-16 編碼的字串,每個位元組使用 2 個字元表示。
    fmt.Printf("%x\n", "hex this")
    //要輸出一個指標的值,使用 %p。
    fmt.Printf("%p\n", &p)
    //當輸出數字的時候,你將經常想要控制輸出結果的寬度和精度,可以使用在 % 後面使用數字來控制輸出寬度。預設結果使用右對齊並且通過空格來填充空白部分。
    fmt.Printf("|%6d|%6d|\n", 12, 345)
    //你也可以指定浮點型的輸出寬度,同時也可以通過 寬度.精度 的語法來指定輸出的精度。
    fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
    //要最對齊,使用 - 標誌。
    fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
    //你也許也想控制字串輸出時的寬度,特別是要確保他們在類表格輸出時的對齊。這是基本的右對齊寬度表示。
    fmt.Printf("|%6s|%6s|\n", "foo", "b")
    //要左對齊,和數字一樣,使用 - 標誌。
    fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
    //到目前為止,我們已經看過 Printf了,它通過 os.Stdout輸出格式化的字串。Sprintf 則格式化並返回一個字串而不帶任何輸出。
    s := fmt.Sprintf("a %s", "string")
    fmt.Println(s)
    //你可以使用 Fprintf 來格式化並輸出到 io.Writers而不是 os.Stdout。
    fmt.Fprintf(os.Stderr, "an %s\n", "error")
}

2、變數

Go 語言變數名由字母、數字、下劃線組成,其中首個字元不能為數字。

宣告變數的一般形式是使用 var 關鍵字:

  • 宣告變數,再賦值

    var name string
    name = "zhangsan" 
    
  • 簡潔賦值,宣告變數+賦值

    name := "zhangsan" 
    

在實際中,採用簡潔賦值可讀性比較好

3、基本型別

Go 的基本型別有

//bool
var ok bool  = true

//string
var name string = "xxx"

//int  int8  int16  int32  int64
//uint uint8 uint16 uint32 uint64 uintptr
var num int = 99

//byte 是 uint8 的別名
var numByte byte = 'c' 

//rune 是 int32 的別名

//float32 float64
var f float32 = 12.1
//複數
//complex64 complex128
//陣列定義
var n [2]int
//陣列定義並賦值
var n = [2]int{1,2}

零值

沒有明確初始值的變數宣告會被賦予它們的零值:

零值是:

  • 數值型別為 0
  • 布林型別為 false
  • 字串為 ""(空字串)。

3.1 字串型別

  • 採用utf-8編碼,不會存在中文亂碼問題
  • 雙引號表示,會識別轉義符
  • 反單引號`表示,不轉移特定字元 ,結合fmt.Sprintf常用於複雜的字串拼接

宣告

var name string
//簡潔方式
name := "xxx" 
package main

import "fmt"

func main() {
	fmt.Println(`反單引號測試\t測試2\n,轉義符不起作用`)
	user := fmt.Sprintf(`{"name":"zhangsan","age":%d}`,10)
	fmt.Println(user)
    //反單引號測試\t測試2\n,轉義符不起作用
	//{"name":"zhangsan","age":10}
}


  • strings標準庫使用C:\Go\src\strings\strings.go
//判斷s是否有後綴字串suffix
func HasSuffix(s, suffix string) bool

//判斷字串s是否包含子串substr
func Contains(s, substr string) bool

//子串sep在字串s中第一次出現的位置,不存在則返回-1。
func Index(s, sep string) int

//返回將所有字母都轉為對應的小寫版本的拷貝。
func ToLower(s string) string

//返回將所有字母都轉為對應的大寫版本的拷貝。
func ToUpper(s string) string

//返回count個s串聯的字串。
func Repeat(s string, count int) string

//返回將s中前n個不重疊old子串都替換為new的新字串,如果n<0會替換所有old子串。
func Replace(s, old, new string, n int) string

//返回將s前後端所有cutset包含的utf-8碼值都去掉的字串。
func Trim(s string, cutset string) string

//用去掉s中出現的sep的方式進行分割,會分割到結尾,並返回生成的所有片段組成的切片(每一個sep都會進行一次切割,即使兩個sep相鄰,也會進行兩次切割)。如果sep為空字元,Split會將s切分成每一個unicode碼值一個字串。
func Split(s, sep string) []string
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.HasSuffix("avatar.png","png"))
	//redis裡獲取一個value裡面
	value:="100:secret"
	index := strings.Index(value,":")
	if index>0 {
		fmt.Println("secret:",value[index+1:])
	}

	fmt.Printf("%q\n", strings.Split("a,b,c", ","))
	
	log("100",20,30)
}

func log(v ... interface{}){
	format := ""
	format+=strings.Repeat("%v ",len(v))
	fmt.Printf(format,v...)
}

3.2 數字型別

// uint8 is the set of all unsigned 8-bit integers.
// Range: 0 through 255.
type uint8 uint8

// uint16 is the set of all unsigned 16-bit integers.
// Range: 0 through 65535.
type uint16 uint16

// uint32 is the set of all unsigned 32-bit integers.
// Range: 0 through 4294967295.
type uint32 uint32

// uint64 is the set of all unsigned 64-bit integers.
// Range: 0 through 18446744073709551615.
type uint64 uint64

// int8 is the set of all signed 8-bit integers.
// Range: -128 through 127.
type int8 int8

// int16 is the set of all signed 16-bit integers.
// Range: -32768 through 32767.
type int16 int16

// int32 is the set of all signed 32-bit integers.
// Range: -2147483648 through 2147483647.
type int32 int32

// int64 is the set of all signed 64-bit integers.
// Range: -9223372036854775808 through 9223372036854775807.
type int64 int64

// float32 is the set of all IEEE-754 32-bit floating-point numbers.
type float32 float32

// float64 is the set of all IEEE-754 64-bit floating-point numbers.
type float64 float64

// int is a signed integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, int32.
// int在32作業系統為32位,最大值是2147483647;在64作業系統中為64位,最大值是9223372036854775807。注意,他不是int32
type int int
/*
package main

import "fmt"

func main() {
	fmt.Println(int(1<<63 -1))
	//9223372036854775807
}

*/

// uint is an unsigned integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, uint32.
type uint uint

/*
package main

import "fmt"

func main() {
	fmt.Println(uint(1<<64 -1))
	//18446744073709551615
}

*/

開發過程中,我們經常用到時間戳,請定義為int64,不然又會埋下像“千年蟲”事件一樣的定時炸彈

2020-11-1 00:00:00 1604160000

2038-01-19 11:14:07 2147483647

3.2 基本型別間轉換

3.2.1 數字型別相互轉換

表示式 T(v) 將值 v 轉換為型別 T

一些關於數值的轉換:

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

或者,更加簡單的形式:

i := 42
f := float64(i)
u := uint(f)

與 C 不同的是,Go 在不同型別的項之間賦值時需要顯式轉換,沒有隱式轉換。

注意一點,大範圍轉小範圍,會出現精度丟失

package main

import "fmt"

//小端模式下
func main() {
    //十六進位制值0x1234 儲存格式為0001 0010 0011 0100
    //int8 只能儲存一個位元組,0011 0100 = 0x34
	var num64 int64 = 0x1234
	fmt.Printf("%x",int8(num64))
    //列印16進製為34
}

補充一個知識點:網路中傳輸的資料統一是大端模式傳輸的,和兩段的系統無關,所以解析網路資料的時候要特別注意,大於一個位元組的數字型別,一定嚴格按照大端模式的資料解析

func tcpReadProto(rd *bufio.Reader, proto *Proto) ([]byte, error) {
	var (
		packLen    int32
		headerLen  int16
		err        error
		decodeBody []byte
	)
	// read
	if err = binary.Read(rd, binary.BigEndian, &packLen); err != nil {
		return nil, err
	}
	log.Debug("packLen: %d", packLen)
	if err = binary.Read(rd, binary.BigEndian, &headerLen); err != nil {
		return nil, err
	}
	log.Debug("headerLen: %d", headerLen)
    ...
}

3.2.2 數字和字串相互轉換

  • 具體程式碼參考 C:\Go\src\strconv包

  • 數字轉字串

//base 指定進位制,必須在2到36之間。
func FormatInt(i int64, base int) string
  • 字串轉數字
//返回字串表示的整數值,接受正負號。
//base指定進位制(2到36),如果base為0,則會從字串前置判斷,"0x"是16進位制,"0"是8進位制,否則是10進位制;
//bitSize指定結果必須能無溢位賦值的整數型別,0、8、16、32、64 分別代表 int、int8、int16、int32、int64;返回的err是*NumErr型別的,如果語法有誤,err.Error = ErrSyntax;如果結果超出類型範圍err.Error = ErrRange。
func ParseInt(s string, base int, bitSize int) (i int64, err error)
  • 簡潔寫法,int範圍內
//Itoa是FormatInt(i, 10) 的簡寫。
func Itoa(i int) string

//Atoi是ParseInt(s, 10, 0)的簡寫。
func Atoi(s string) (i int, err error)
  • 例子
package main

import (
	"fmt"
	"log"
	"strconv"
)

func main() {
	str := strconv.FormatInt(111,10)
	fmt.Printf("str is %s \n",str)

	num,err := strconv.ParseInt("666",10,64)
	if err !=nil {
		log.Fatal(err.Error())
	}
	fmt.Printf("num is %d \n",num)


	str2 := strconv.Itoa(99)
	fmt.Printf("str2 is %s \n",str2)
	num2,_ := strconv.Atoi("99")
	fmt.Printf("num2 is %d \n",num2)
}


4 常量

  • 常量在程式執行時,不可被修改。
  • 常量中的資料型別只可以是布林型、數字型和字串型。
  • 常量不能用 := 語法宣告。
  • 用const 定義

iota是一個預先宣告的識別符號,iota 在 const關鍵字出現時將被重置為 0,const 中每新增一行常量宣告將使 iota 計數一次,即下一行=前一行+1

package main

import "fmt"

func main() {
	const (
		Unknown = iota
		Female
		Male
	)
	fmt.Println(Unknown,Female,Male)
    //0 1 2
}

5 語句

5.1 條件語句 if

if 表示式 {
   /* 在表示式為 true 時執行 */
}

Go 的 if 語句與 for 迴圈類似,表示式外無需小括號 ( ) ,而大括號 { } 則是必須的。

import (
	"fmt"
)

func check() (string,bool) {
	return "namespace",true
}

func main() {
	if ns,ok := check(); ok {
		fmt.Println(ns)
	}
    //注意ns的作用域在條件語句{}內,下列會編譯會報錯undefined: ns
	//fmt.Println(ns)
}

5.2 條件語句 switch

//switch語句將expr表示式結果與可能的value值的列表進行匹配,然後執行響應程式碼
switch expr {
    case vaule:
        ...
    case vaule:
        ...
    default:
        ...
}

switch 是編寫一連串 if - else 語句的簡便方法。它執行第一個值等於條件表示式的 case 語句。

Go 的 switch 語句類似於 C、C++、Java、JavaScript 和 PHP 中的,不過 Go 只執行選定的 case,而非之後所有的 case。 實際上,Go 自動提供了在這些語言中每個 case 後面所需的 break 語句。 除非以 fallthrough 語句結束,否則分支會自動終止。 Go 的另一點重要的不同在於 switch 的 case 無需為常量,且取值不必為整數。

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Print("Go runs on ")
	os := runtime.GOOS
	switch  os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		fmt.Printf("%s.\n", os)
	}
}

注意:switch 的 case 語句從上到下順次執行,直到匹配成功時停止。

package main

import "fmt"

func main() {
	switch 3 {
	case f1():
		fmt.Println("execute 1 case statement")
	case f2():
		fmt.Println("execute 2 case statement")
	case f3():
		fmt.Println("execute 3 case statement")
	case f4():
		fmt.Println("execute 4 case statement")
	default:
		fmt.Println("execute default statement")
	}
}

func f1() int {
	fmt.Println("f1 function")
	return 1
}

func f2() int {
	fmt.Println("f2 function")
	return 2
}
func f3() int {
	fmt.Println("f3 function")
	return 3
}
func f4() int {
	fmt.Println("f4 function")
	return 4
}

5.2.1 沒有條件的 switch

沒有條件的 switch 同 switch true 一樣。

這種形式能將一長串 if-then-else 寫得更加清晰。

package main

import (
	"fmt"
)

func main() {
	score:=55
	if score>90 {
		fmt.Println("優秀")
	}else if score>80{
		fmt.Println("良")
	}else if score>70{
		fmt.Println("中")
	}else if score>60{
		fmt.Println("及格")
	}else {
		fmt.Println("不及格")
	}

	switch {
	case  score>90:
		fmt.Println("優秀")
	case  score>80:
		fmt.Println("良")
	case  score>70:
		fmt.Println("中")
	case  score>60:
		fmt.Println("及格")
	default:
		fmt.Println("不及格")
	}
}

5.2.2 fallthrough

使用 fallthrough 會強制執行後面的 case 語句,fallthrough 不會判斷下一條 case 的表示式結果是否為 true。

package main

import "fmt"

func main() {

    switch {
    case false:
            fmt.Println("1、case 條件語句為 false")
            fallthrough
    case true:
            fmt.Println("2、case 條件語句為 true")
            fallthrough
    case false:
            fmt.Println("3、case 條件語句為 false")
            fallthrough
    case true:
            fmt.Println("4、case 條件語句為 true")
    case false:
            fmt.Println("5、case 條件語句為 false")
            fallthrough
    default:
            fmt.Println("6、預設 case")
    }
}

5.3 迴圈語句

5.3.1 for

for 初始化; 控制條件語句; 控制條件賦值 {
//控制條件語句為true執行迴圈體
}

例子

package main

import "fmt"

func main() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}
}

5.3.2 for range

for 迴圈的 range 格式可以對 slice、map、陣列、字串等進行迭代迴圈。格式如下:

for key, value := range arr {
    //獲取key,value,然後對應操作
}

例子

package main

import "fmt"

func main() {
	nums := []int{1, 2, 3, 5}
	for key, value := range nums {
		fmt.Printf("key=%d, value=%d\n", key, value)
	}
}
/* 
結果為
key=0, value=1
key=1, value=2
key=2, value=3
key=3, value=5
*/

5.3.3 continue

  • 跳過當前迴圈的剩餘語句,然後繼續進行下一輪迴圈。

5.3.4 break

  • 跳出迴圈,並開始執行迴圈之後的語句。
  • break有坑

for迴圈在和switch語句,select語句連用時候,使用break不當,容易造成死迴圈

break是跳出最近的for迴圈,或者switch和select語句

可以實驗下面程式碼,break語句無法跳出迴圈

package main

import (
	"fmt"
	"time"
)

func main() {
	for i := 1; ; i++ {
		time.Sleep(time.Second)
		switch i {
		case 1:
			fmt.Println("case 1,i=", i)
		case 2:
			fmt.Println("case 2,i=", i)
		default:
			fmt.Println("case default", i)
			break
		}
	}
}

解決辦法:

  • 可以加標籤,指定break出那一層。建議不要這樣做,程式碼不簡潔,可讀性差。
  • 可以封裝switch語句在函式內,函式自帶作用域範圍,就不會引起這樣問題了。
package main

import (
	"fmt"
	"time"
)
func main() {
re:
	for i := 1; ; i++ {
		time.Sleep(time.Second)
		switch i {
		case 1:
			fmt.Println("case 1,i=", i)
		case 2:
			fmt.Println("case 2,i=", i)
		default:
			fmt.Println("case default", i)
			break re
		}
	}
}
package main

import (
	"fmt"
	"time"
)

func f(i int) {
	switch i {
	case 1:
		fmt.Println("case 1,i=", i)
	case 2:
		fmt.Println("case 2,i=", i)
	default:
		fmt.Println("case default", i)
	}
}

func main() {
	for i := 1; ; i++ {
		time.Sleep(time.Second)
		f(i)
		break
	}
}

5.3.4 for range的坑

  • 切記,不要對for range 的引用value的值

  • key和value只定義一次,在迴圈體內每迴圈一次做一次賦值。他們的地址是不變化的

package main

import (
	"fmt"
)

func main() {
	nums := []int{1, 3, 5, 7}
	nums2 := make([]*int, 0)
	for key, value := range nums {
		nums2 = append(nums2, &value)
		fmt.Printf("key=%d,keyp=%p, value=%d,valuep=%p\n", key, &key, value, &value)
	}
	for _, v := range nums2 {
		fmt.Printf("p=%p,value=%d\n", v, *v)
	}
}

go for range原始碼

https://github.com/golang/gofrontend/blob/master/go/statements.cc

For_range_statement::lower_range_slice

  // The loop we generate:
  //   for_temp := range
  //   len_temp := len(for_temp)
  //   for index_temp = 0; index_temp < len_temp; index_temp++ {
  //           value_temp = for_temp[index_temp]
  //           index = index_temp
  //           value = value_temp
  //           original body
  //   }

6 指標

Go 擁有指標。指標儲存了值的記憶體地址。

型別 *T 是指向 T 型別值的指標。其零值為 nil

var p *int

& 操作符會生成一個指向其運算元的指標。

i := 42
p = &i

* 操作符表示指標指向的底層值。

fmt.Println(*p) // 通過指標 p 讀取 i
*p = 21         // 通過指標 p 設定 i

這也就是通常所說的“間接引用”或“重定向”。

與 C 不同,Go 沒有指標運算。

7 結構體

一個結構體(struct)就是一組欄位(field)。

結構體欄位使用點號來訪問。

結構體欄位可以通過結構體指標來訪問。

package main

import "fmt"

type Vertex struct {
	X, Y int
}

var (
	v1 = Vertex{1, 2}       // 建立一個 Vertex 型別的結構體
	v2 = Vertex{X: 5, Y: 6} //指定成員賦值,與順序無關
	v3 = Vertex{}           // X:0 Y:0
	p  = &Vertex{9, 10}     // 建立一個 *Vertex 型別的結構體(指標)
)

func main() {
	fmt.Println("v1.x = ", v1.X)
	fmt.Println("p.x = ", p.X)
	fmt.Println(v1, v2, v3, p)
}


8 陣列

  • 型別 [n]T 表示擁有 nT 型別的值的陣列。
  • 陣列的長度是其型別的一部分,陣列不能改變大小
  • 表示式
var a [10]int //宣告
var f = [5]float32{1.0, 2.0, 3.1, 4.6, 20.1} //宣告並賦值

會將變數 a 宣告為擁有 10 個整數的陣列。

package main

import "fmt"

func main() {
	var a [2]string
	a[0] = "Hello"
	a[1] = "World"
	fmt.Println(a[0], a[1])
	fmt.Println(a)

	primes := [6]int{2, 3, 5, 7, 11, 13}
	fmt.Println(primes)
}

9 切片

9.1 宣告

切片宣告類似於沒有長度的陣列文法。

這是一個數組文法:

//[3]int{1, 2, 3}
var bools = [3]int{1, 2, 3}

下面這樣則會建立一個和上面相同的陣列,然後構建一個引用了它的切片:

//[]int{1, 2, 3}
var s = []int{1, 2, 3}

每個陣列的大小都是固定的。而切片則為陣列元素提供動態大小的、靈活的視角。在實踐中,切片比陣列更常用。

型別 []T 表示一個元素型別為 T 的切片。

切片通過兩個下標來界定,即一個上界和一個下界,二者以冒號分隔:

a[0 : 3] 

它會選擇一個半開區間,包括第一個元素,但排除最後一個元素。

以下表達式建立了一個切片,它包含 a 中下標從 1 到 3 的元素:

a[1:4]

例子:

package main

import "fmt"

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	var s []int = primes[1:4]
	fmt.Println(s)
}

nil是一個預先宣告的識別符號,表示指標、通道、函式、介面、對映或切片型別的零值。

9.2 切片就像陣列的引用

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

切片並不儲存任何資料,它只是描述了底層陣列中的一段。

更改切片的元素會修改其底層陣列中對應的元素。

與它共享底層陣列的切片都會觀測到這些修改。

package main

import "fmt"

func main() {
	names := [4]string{
		"John",
		"Paul",
		"George",
		"Ringo",
	}
	fmt.Println(names)

	a := names[0:2]
	b := names[1:3]
	fmt.Println(a, b)

	b[0] = "XXX"
	fmt.Println(a, b)
	fmt.Println(names)
}

9.3 切片的預設行為

在進行切片時,你可以利用它的預設行為來忽略上下界。

切片下界的預設值為 0,上界則是該切片的長度。

對於陣列

var a [10]int

來說,以下切片是等價的:

a[0:10]
a[:10]
a[0:]
a[:]
package main

import "fmt"

func main() {
	s := []int{2, 3, 5, 7, 11, 13}

	s = s[1:4]
	fmt.Println(s)

	s = s[:2]
	fmt.Println(s)

	s = s[1:]
	fmt.Println(s)
}

9.4 切片的長度與容量

切片擁有 長度容量

切片的長度就是它所包含的元素個數。

切片的容量是從它的第一個元素開始數,到其底層陣列元素末尾的個數。

切片 s 的長度和容量可通過表示式 len(s)cap(s) 來獲取。

你可以通過重新切片來擴充套件一個切片,給它提供足夠的容量。試著修改示例程式中的切片操作,向外擴充套件它的容量,看看會發生什麼。

package main

import "fmt"

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	printSlice(s)

	// 擷取切片使其長度為 0
	s = s[:0]
	printSlice(s)

	// 拓展其長度
	s = s[:4]
	printSlice(s)

	// 捨棄前兩個值
	s = s[2:]
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

9.5 切片零值

切片的零值是 nil

nil 切片的長度和容量為 0 且沒有底層陣列。

package main

import "fmt"

func main() {
	var s []int
	fmt.Println(s, len(s), cap(s))
	if s == nil {
		fmt.Println("nil!")
	}
}

9.6 用 make 建立切片

切片可以用內建函式 make 來建立,這也是你建立動態陣列的方式。

make 函式會分配一個元素為零值的陣列並返回一個引用了它的切片:

a := make([]int, 5)  // len(a)=5

要指定它的容量,需向 make 傳入第三個引數:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

9.7 向切片追加元素

為切片追加新的元素是種常用的操作,為此 Go 提供了內建的 append 函式。內建函式的文件對此函式有詳細的介紹。

func append(s []T, vs ...T) []T

append 的第一個引數 s 是一個元素型別為 T 的切片,其餘型別為 T 的值將會追加到該切片的末尾。

append 的結果是一個包含原切片所有元素加上新新增元素的切片。

s 的底層陣列太小,不足以容納所有給定的值時,它就會分配一個更大的陣列。返回的切片會指向這個新分配的陣列。

package main

import "fmt"

func main() {
	var s []int
	printSlice(s)

	// 新增一個空切片
	s = append(s, 0)
	printSlice(s)

	// 這個切片會按需增長
	s = append(s, 1)
	printSlice(s)

	// 可以一次性新增多個元素
	s = append(s, 2, 3, 4)
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

9.8 切片擴容機制

  • 當原切片長度小於1024時,新切片的容量會直接翻倍。而當原切片的容量大於等於1024時,會反覆地增加25%,直到新容量超過所需要的容量。
  • 若新入元素大小超過了原有的容量,則新容量取兩者相加計算出來的最小cap值。
  • 出於記憶體的高效利用考慮,還要進行記憶體對齊

src/runtime/slice.go

	...
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	...

下列實驗觀察切片容量增長

package main

import "fmt"

var sCap int

func main() {
	var s []int
	for i := 0; i < 100000; i++ {
		s = append(s, i)
		printCapChange(s)
	}
}
func printCapChange(s []int) {
	oldCap := sCap
	sCap = cap(s)
	if oldCap != sCap && oldCap > 0 {
		fmt.Printf("cap change %d %d \n", sCap, (sCap)*100/oldCap)
	}
}

切片指向的陣列,每次擴容陣列會發生變化實驗

package main

import (
	"fmt"
)

var sCap int

func main() {
	var arr = [4]int{1, 2, 3, 4}
	var s = arr[0:4]
	fmt.Printf("陣列 %v ,%p\n", arr, &arr)
	fmt.Printf("切片 %v ,%p\n", s, s)
	for i := 0; i < 20; i++ {
		s = append(s, i)
		printCapChange(s)
	}
	s[0] = 999
	fmt.Printf("陣列%v ,%p\n", arr, &arr)
	fmt.Printf("切片底層的陣列已經改變 %v ,%p\n", s, s)
}
func printCapChange(s []int) {
	oldCap := sCap
	sCap = cap(s)
	if oldCap != sCap && oldCap > 0 {
		fmt.Printf("切片%p 容量改變為%d,擴大為原來%d%% \n", &s, sCap, (sCap)*100/oldCap)
	}
}

9.9 切片引起的bug

s := arr[0:4]
//當你改變切片元素,底層的陣列或指向該陣列的其他切片,都可能會被修改,所以,如有修改操作,請使用copy函式
s[0] = 999 

9.10 切片copy操作

//dst目標切片,目標切片的大小要和源切片大小一樣;src源切片
func copy(dst, src []Type) int

10 map

  • 對應其他語言字典或hash表
  • map是一種無序的鍵值對的集合
  • map的零值是nil,可以用make來建立
  • map併發讀寫不安全,不能同時併發讀寫,不能同時併發寫
  • 如果只用來讀,是併發安全的

10.1 map操作

map宣告

//使用make宣告並初始化map
m := make(map[int]int)

//宣告並初始化map
var score = map[string]int{"zhangsan": 99, "lisi": 88, "wangwu": 59} 

//簡潔宣告並初始化map
score1 := map[string]int{"zhangsan": 99, "lisi": 88, "wangwu": 59}

在map m 中插入或修改元素:

m[key] = elem

獲取元素:

elem = m[key]

刪除元素:

delete(m, key)

通過雙賦值檢測某個鍵是否存在:

elem, ok = m[key]

keym 中,oktrue ;否則,okfalse

key 不在map中,那麼 elem 是該map元素型別零。

同樣的,當從map中讀取某個不存在的鍵時,結果是map射的元素型別的零值。

:若 elemok 還未宣告,你可以使用短變數宣告:

elem, ok := m[key]

10.2 map併發不安全

下面程式碼可能會出現

  • 併發讀寫錯誤 fatal error: concurrent map read and map write

  • 併發寫錯誤 fatal error: concurrent map writes

package main

import "time"

func main() {
	m := make(map[int]int)

	for i := 0; i < 100; i++ {
		go writeMap(m, i, i)
		go readMap(m, i)
	}
	time.Sleep(time.Second)
}

func readMap(m map[int]int, key int) int {
	return m[key]
}

func writeMap(m map[int]int, key int, value int) {
	m[key] = value
}

10.3 map加鎖版本

package main

import (
	"fmt"
	"math/rand"
	"strconv"
	"sync"
)

type userMap struct {
	sync.RWMutex
	m map[string]string
}

func (u *userMap) read(key string) {
	u.RLock()
	defer u.RUnlock()
	n := u.m[key]
	fmt.Println("value:", n)
}

func (u *userMap) write(key, value string) {
	u.Lock()
	defer u.Unlock()
	u.m[key] = value
}
func main() {
	c := userMap{
		m: make(map[string]string),
	}
	for i := 0; i < 1000; i++ {
		v := rand.Int31n(9999)
		go c.write(strconv.Itoa(i), strconv.Itoa(int(v)))
		go c.read(strconv.Itoa(i))

	}
}

11 函式

11.1 函式定義

  • 定義:函式名、形參列表、返回值列表、函式體
func function_name( [parameter list] ) [return_types] {
   函式體
}

func funcName(param1 int,param2 string)(int, error){
	return 0,nil
}
  • 多返回值

  • 函式大寫開頭表示public,包外面可見。小寫開頭表示private,包外部可見

  • 引數傳遞是值傳遞

    基礎型別、陣列、結構體,在函式內修改,不會影響到原來的值。

    slicemapchannel本身是引用型別,在函式內修改,會影響原來的值

  • 函式也可以當做變數

  • 可變引數,用 表示

func myfunc(args ...int) {
	for _, arg := range args {
		fmt.Println(arg)
	}
}

11.1 retrun

  • Go 語言支援多個返回值
  • 函式返回是分兩步的,1、先給返回值賦值,2、然後再返回
package main

func foo() (int, int) {
	i := 1
	j := 2
	return i, j
}

func main() {
	foo()
}

檢視彙編程式碼go tool compile -S -N -l types.go

0x0000 00000 (types.go:3)       TEXT    "".foo(SB), NOSPLIT|ABIInternal, $24-16
        0x0000 00000 (types.go:3)       SUBQ    $24, SP
        0x0004 00004 (types.go:3)       MOVQ    BP, 16(SP)
        0x0009 00009 (types.go:3)       LEAQ    16(SP), BP
        0x000e 00014 (types.go:3)       PCDATA  $0, $-2
        0x000e 00014 (types.go:3)       PCDATA  $1, $-2
        0x000e 00014 (types.go:3)       FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (types.go:3)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (types.go:3)       FUNCDATA        $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (types.go:3)       PCDATA  $0, $0
        0x000e 00014 (types.go:3)       PCDATA  $1, $0
        0x000e 00014 (types.go:3)       MOVQ    $0, "".~r0+32(SP)
        0x0017 00023 (types.go:3)       MOVQ    $0, "".~r1+40(SP)
        0x0020 00032 (types.go:4)       MOVQ    $1, "".i+8(SP)
        0x0029 00041 (types.go:5)       MOVQ    $2, "".j(SP)
        0x0031 00049 (types.go:6)       MOVQ    "".i+8(SP), AX
        0x0036 00054 (types.go:6)       MOVQ    AX, "".~r0+32(SP)
        0x003b 00059 (types.go:6)       MOVQ    "".j(SP), AX
        0x003f 00063 (types.go:6)       MOVQ    AX, "".~r1+40(SP)
        0x0044 00068 (types.go:6)       MOVQ    16(SP), BP
        0x0049 00073 (types.go:6)       ADDQ    $24, SP
        0x004d 00077 (types.go:6)       RET

11.2 defer

  • 在函式退出時執行
  • 多個defer按先進後出方式執行
  • defer呼叫在返回值賦值後,在函式返回前
返回值賦值
defer f()
return
package main

import "fmt"
//1、返回值為定義一個臨時變數,賦值為1
//2、defer中i++
//3、return;所以返回值是1,defer中i改變不影響返回值
func foo() int {
	i := 1
	defer func() {
		i++
		fmt.Printf("i=%d\n", i)
	}()
	return i
}

func main() {
	res := foo()
	fmt.Printf("返回值=%d\n", res)
}

返回值如果是命名返回值,情況又不一樣

package main

import "fmt"
//1、i=1
//2、defer中i=11
//3、return
func foo() (i int) {
   defer func() {
      i = i + 10
   }()
   return 1
}

func main() {
   res := foo()
   fmt.Printf("res=%d\n", res)
}

11.3 閉包函式

閉包是可以包含自由(未繫結到特定物件)變數的程式碼塊,這些變數不在這個程式碼塊內或者
任何全域性上下文中定義,而是在定義程式碼塊的環境中定義。要執行的程式碼塊(由於自由變數包含
在程式碼塊中,所以這些自由變數以及它們引用的物件沒有被釋放)為自由變數提供繫結的計算環
境(作用域)。

應用《Go語言程式設計》的描述,我們可以理解閉包為帶有狀態的函式,由變數和函式兩部分組成。

package main

import (
	"fmt"
	"strconv"
)

func add() func(int) int {
	n := 10
	str := "good luck"
	return func(x int) int {
		n = x + n
		str = str+strconv.Itoa(x)
		fmt.Println(str)
		return n
	}
}

func main() {
	fn := add()
	fmt.Printf("%d\n", fn(1))
	fmt.Printf("%d\n", fn(1))
	fmt.Printf("%d\n", fn(1))
}

閉包可以看做下面變數和匿名函式的結合

	n := 10
	str := "good luck"
	return func(x int) int {
		n = x + n
		str += strconv.Itoa(x)
		fmt.Println(str)
		return n
	}

12 介面interface

12.1 Any型別

由於Go語言中任何物件例項都滿足空介面interface{},所以interface{}看起來像是可
以指向任何物件的Any型別,如下:

var v1 interface{} = 1 // 將int型別賦值給interface{}
var v2 interface{} = "abc" // 將string型別賦值給interface{}
var v3 interface{} = &v2 // 將*interface{}型別賦值給interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}

當函式可以接受任意的物件例項時,我們會將其宣告為interface{},最典型的例子是標
準庫fmt中PrintXXX系列的函式,例如:

func Printf(fmt string, args ...interface{})
func Println(args ...interface{})
...

12.2 型別斷言

型別斷言 提供了訪問介面值底層具體值的方式。

t := i.(T)

該語句斷言介面值 i 儲存了具體型別 T,並將其底層型別為 T 的值賦予變數 t

i 並未儲存 T 型別的值,該語句就會觸發一個恐慌。

為了 判斷 一個介面值是否儲存了一個特定的型別,型別斷言可返回兩個值:其底層值以及一個報告斷言是否成功的布林值。

t, ok := i.(T)

i 儲存了一個 T,那麼 t 將會是其底層值,而 oktrue

否則,ok 將為 falset 將為 T 型別的零值,程式並不會產生恐慌。

請注意這種語法和讀取一個對映時的相同之處

package main

import "fmt"

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64)
	fmt.Println(f, ok)

	f = i.(float64) // 報錯(panic)
	fmt.Println(f)
}

12.3 型別選擇

型別選擇 是一種按順序從幾個型別斷言中選擇分支的結構。

型別選擇與一般的 switch 語句相似,不過型別選擇中的 case 為型別(而非值), 它們針對給定介面值所儲存的值的型別進行比較。

switch v := i.(type) {
case T:
    // v 的型別為 T
case S:
    // v 的型別為 S
default:
    // 沒有匹配,v 與 i 的型別相同
}

型別選擇中的宣告與型別斷言 i.(T) 的語法相同,只是具體型別 T 被替換成了關鍵字 type

此選擇語句判斷介面值 i 儲存的值型別是 T 還是 S。在 TS 的情況下,變數 v 會分別按 TS 型別儲存 i 擁有的值。在預設(即沒有匹配)的情況下,變數 vi 的介面型別和值相同。

package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

12.4 非侵入式介面

在Go語言中,一個類只需要實現了介面要求的所有函式,我們就說這個類實現了該介面,
例如:

type File struct {
// ...
}
func (f *File) Read(buf []byte) (n int, err error){
   ... 
}
func (f *File) Write(buf []byte) (n int, err error){
   ... 
}
func (f *File) Seek(off int64, whence int) (pos int64, err error){
   ... 
}
func (f *File) Close() error{
   ... 
}

這裡我們定義了一個File類,並實現有Read()、Write()、Seek()、Close()等方法。設
想我們有如下介面:

type IFile interface {
	Read(buf []byte) (n int, err error)
	Write(buf []byte) (n int, err error)
	Seek(off int64, whence int) (pos int64, err error)
	Close() error
}
type IReader interface {
	Read(buf []byte) (n int, err error)
}
type IWriter interface {
	Write(buf []byte) (n int, err error)
}
type ICloser interface {
	Close() error
}

儘管File類並沒有從這些介面繼承,甚至可以不知道這些介面的存在,但是File類實現了
這些介面,可以進行賦值:

var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)

13 協程goroutine

  • goroutine 是輕量級執行緒,goroutine 的排程是由 Golang 執行時進行管理的。

  • 執行緒和goroutine區別

    在linux作業系統中,執行緒是一種輕量級程序,由作業系統來排程。執行緒切換需要由使用者態切換到核心態進行,將一些執行緒資源儲存在核心記憶體空間(32位系統是3-4G),然後從核心記憶體空間恢復被排程的下一個執行緒資源。

    goroutine是真正意義上的執行緒,go實現了goroutine的管理,goroutine的切換隻在使用者態進行。

  • Don’t communicate by sharing memory; share memory by communicating.不要通過共享資料來通訊,恰恰相反,要以通訊的方式共享資料。

  • 在業務量併發大的情況下,請注意控制goroutine數量,你後端的程式效能跟不上(資料庫),你程式併發支援再多也沒用。請使用協程池。

goroutine 語法格式:

go 函式名(引數列表)

14 channel

  • channel是併發安全的

  • 型別字面量如chan int,其中的chan是表示通道型別的關鍵字,而int則說明了該通道型別的元素型別

  • 可以使用make方法來建立,第二個引數代表通道緩衝容量,0或者沒有代表非緩衝通道

  • 是引用型別,零值是nil,寫或讀零值channel,會一直阻塞

  • 通道是先進先出。阻塞時,傳送有傳送佇列,接收有接收佇列,通道可傳送或可接收時,通知佇列裡面最新等待發送或接收的goroutine

  • 資料進入通道是複製右邊的元素到通道(注意引用型別,只複製元素本身,他指向的資料還是不變)

  • channel在close操作後,繼續傳送資料會導致panic。close的通道可以繼續讀取資料,可以定義兩個變數接收讀取通道的結果

  • 一般在傳送端關閉通道

    //通道關閉的時候,讀取完通道資料後,ok=false,所以在讀取的時候注意判斷ok的值
    value, ok := <-c
    
  • 阻塞情況

    有緩衝區的通道:1、寫入緩衝已滿的通道會阻塞。2、讀取沒有資料的通道。

    無緩衝通道:傳送或接收有一方沒準備,就會阻塞在那裡

14.1 宣告

//未賦值的channel的值是nil
var c chan int
//宣告並賦值
var c = make(chan int, 10)
c := make(chan int, 10)
  • 寫入和讀取chan,用<-表示
package main

import (
	"fmt"
	"time"
)

func main() {
	//var c chan int = make(chan int, 10)
    c := make(chan int, 10)
	//寫入
    c <- 1
	c <- 2
	c <- 3
	go func() {
		for {
            //讀取
			value, ok := <-c
			if ok {
				fmt.Println(value)
			} else {
				fmt.Println("channel close")
				time.Sleep(2 * time.Second)
			}
		}
	}()
	c <- 4
	close(c)
	time.Sleep(2 * time.Second)
}

14.2 channel的for range

  • 這樣一條for語句會不斷地嘗試從chan種取出元素值,即使chan被關閉,它也會在取出所有剩餘的元素值之後再結束執行。
package main

import (
	"fmt"
)

func main() {
	num := 5
	ch := make(chan int, num)
	for i := 0; i < num; i++ {
		ch <- i
	}
	close(ch)
	for elem := range ch {
		fmt.Printf("The element is: %v\n", elem)
	}
	fmt.Println("666")
}

  • 當chan中沒有元素值時,它會被阻塞在有for關鍵字的那一行,直到有新的元素值可取。
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 5)
	go func() {
		for elem := range ch {
			fmt.Printf("The element is: %v\n", elem)
		}
	}()
	time.Sleep(5 * time.Second)
	fmt.Println("start send element")
	for i := 0; i < 5; i++ {
		ch <- i
	}
	close(ch)
	fmt.Println("close channel")
	time.Sleep(1 * time.Second)
}

  • 假設chan的沒有賦值,那麼它會被永遠阻塞在有for關鍵字的那一行。
/*
這段程式碼會報fatal error: all goroutines are asleep - deadlock!
所有協程都阻塞,for後面的程式碼執行不到了
*/
package main

import (
	"fmt"
)

func main() {
	var ch chan int
	for elem := range ch {
		fmt.Printf("The elem is: %v\n", elem)
	}
	fmt.Println("666")
}

14.3 select

  • select語句是專為通道而設計的,每個case表示式中都只能包含操作通道的表示式,接收表示式和傳送表示式。
  • 進入select語句時,所有case的chan都阻塞情況下,會執行default語句
  • select語句發現同時有多個候選case分支滿足選擇條件,那麼它就會用一種偽隨機的演算法在這些分支中選擇一個並執行
  • 僅當select語句中的所有case表示式都被求值完畢後,它才會開始選擇候選分支。
  • select中也是有break語法,在for迴圈中的select要注意,break是跳出最近的select、switch、for。請注意break的坑
//多執行幾次該程式,可以看到select的case分支是隨機的
//多執行幾次該程式,可以看到select的case分支是隨機的
package main

import (
	"fmt"
	"math/rand"
	"time"
)

func send(ch chan int) {
	value := rand.Int31n(9999)
	ch <- int(value)
}
func main() {
	rand.Seed(time.Now().UnixNano())
	ch1 := make(chan int, 1)
	ch2 := make(chan int, 1)
	send(ch1)
	send(ch2)
	time.Sleep(1000)
	timer := time.NewTimer(3 * time.Second).C
loop:
	for {
		select {
		case v := <-ch1:
			fmt.Println("ch1通道獲取到資料", v)
		case v := <-ch2:
			fmt.Println("ch2通道獲取到資料", v)
		case <-timer:
			fmt.Println("退出for迴圈")
			break loop
			//default:
			//	time.Sleep(1000)
			//	fmt.Println("通道中沒有資料")
		}
	}
}


注意:case表示式都是先求值的,可以實驗下面例子

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func send(ch chan int) {
	value := rand.Int31n(9999)
	ch <- int(value)
}

func recv(ch chan int, id int) <-chan int {
	fmt.Println(id, "在這裡可以進行一些邏輯")
	return ch
}
func main() {
	rand.Seed(time.Now().UnixNano())
	ch1 := make(chan int, 1)
	ch2 := make(chan int, 1)
	send(ch1)
	send(ch2)
	time.Sleep(1000)
	timer := time.NewTimer(3 * time.Second).C
loop:
	for {
		select {
		case v := <-recv(ch1, 1):
			fmt.Println("ch1通道獲取到資料", v)
		case v := <-recv(ch2, 2):
			fmt.Println("ch2通道獲取到資料", v)
		case <-timer:
			fmt.Println("退出for迴圈")
			break loop
			//default:
			//	fmt.Println("通道中沒有資料")
		}
	}

}

/*結果
1 在這裡可以進行一些邏輯
2 在這裡可以進行一些邏輯
ch1通道獲取到資料 795
1 在這裡可以進行一些邏輯
2 在這裡可以進行一些邏輯
ch2通道獲取到資料 5534
1 在這裡可以進行一些邏輯
2 在這裡可以進行一些邏輯
退出for迴圈
*/

14.4 單方向channel的應用

var send chan<- int// 只能傳送
var recv <-chan int // 只能接收

// 一般用於介面中約束某個方法中的channel只能用於傳送資料或者接收資料,在傳入引數的時候是可以傳雙向的channel
type Notifier interface {
	SendInt(ch chan<- int)
	RcevInt(ch <-chan int)
}