Go工程師體系課 001

8次閱讀

轉型

  1. 想在短時間系統轉到 Go 工程理由
  2. 提高 CRUD,無自研框架經驗
  3. 拔高技術深度,做專、做精需求的同學
  4. 進階工程化,擁有良好開發規範和管理能力的

工程化的重要性

高級開的期望

  1. 良好的代碼規範
  2. 深入底層原理
  3. 熟悉架構
  4. 熟悉 k8s 的基礎架構

擴展知識廣度,知識的深度,規範的開發體系

四個大的階段

  1. go 語言基礎
  2. 微服務開發的(電商項目實戰)
  3. 自研微服務
  4. 自研然後重構

領域

  1. Web 開發 -gin、beego 等
  2. 容器虛擬化 -docker、k8s、istio
  3. 中間件 - etcd、tidb、influxdb、nsq 等
  4. 區塊鏈 以太坊、fabric
  5. 1xAR5 – go-zero, dapr, rpcx, kratos. dubbo-go,

相關環境的開發

  1. go 安裝 官網
  2. goland、vscdoe
# go build hello.go
# ./hello
go run hello.go

golang新版本中同一個包下有多個 main 運行時,運行工具中選擇運行類型file, go 中不推薦一個目錄中有兩個 main 文件,如果需要有多個 main 放到不同的文件夾中

變量

package main

import "fmt"

// 全局變量 定義可以不使用
var name = "bobby"
var age = 19
var ok bool

func main() {
 // go 是靜態語言 靜態語言和動態語言相比變量差異很大
 // 1. 變更定義 先定義後使用 類型聲明瞭不能改變
 // 定義變量
 var name string = "hello world"
 age := 1
 // go 語言中變量定義的不使用是不行的,強制性的
 fmt.Println(name, age)

 // 多變量定義
 var user1, user2, user3 = "booby1", "bobby2", 22
 fmt.Println(user1, user2, user3)

 // 注意變量先定義才能使用
 // 是靜態語言類型和賦值要一致
 // 要求和規範性會更好
 // 變量名不能衝突 同一個代碼塊中不能同名的
 // 簡潔定義 名:= 值 不能用於全局定義
 // 變量是有 0 值的

}

變量

package main

import "fmt"

func main() {
 // 常量 定義的時候就要指定值,不能修改
 const PI float32 = 3.14159265358979323846 // 顯式定義
 const PI2 = 3.14159265358979323846        // 常量 大寫多單詞 用下劃線
 const (
  UNKNOW = 1
  FEMALE = 2
  MALE   = 3
 )
 // 沒有設置類型和值它會沿用前面的值
 const (
  x int = 16
  y
  s = "abc"
  z
 )
 fmt.Println(x, y, z)
 /**
 常量類型只要以是 bool 數值(整數、浮點)不曾使用的常量,沒有強制使用的
 顯示指定類型的時候,必須確保左右類型一致
 */
}

iota

特殊常量

package main

import "fmt"

func main() {
 // 匿名變量 定義一個變更不使用它
 var _ int // 接收返回值進,點位符只接收哪個值
 // iota 特殊常量,可以認爲是一種可以被編譯器修改的常量
 const (
  ERR1 = iota + 1
  ERR2
  ERR25 = "ha" // iota 內部仍然增加計數
  ERR3
  ERR4 = iota
 )
 const (ERRORNEW1 = iota // 從 0 開始計數)
 /*
   如果中斷了 iota 那麼要顯示的恢復,後續會自動遞增 自增類型默認是 int 類型的
   iota 能簡化 const 類型的定義
 */
 fmt.Println(ERR1, ERR2, ERR3, ERR4)
}

變量的作用域

個人感覺注意代碼塊的範圍還有就是 變量名:= 值 這種方式的聲明

go 的基本數據類型

  • 基本數據類型
  • bool true false
  • 數值類型
    • 整數 int8~64 uint8~64(無符號數)
    • 浮點數 float32 float64
    • 複數
    • byte 字節 uint8
    • rune 類型 int32
  • 字符和 string
package main

import "fmt"

func main() {
 var a int8
 var b int16
 var c int32
 var d int64

 var ua uint8
 var ub uint16
 var uc uint32

 var ch3 byte
 c = 'a'         // 字符類型
 var int_32 rune // 也是字符

 int_32 = '中' // 即有中文也有英文
 fmt.Println(int_32)

 fmt.Print("c=%c", ch3)

 var str string
 str = "i am bobby"
 fmt.Println(str)


} 

各種類型的間的轉換

 var str string
 str = "i am bobby"
 fmt.Println(str)
 // 字符串轉數字
 var istr = "12"
 myint, err := strconv.Atoi(istr)
 if err != nil {fmt.Println("convert error")
 }
 fmt.Println(myint)

 var myi = 32
 mstr := strconv.Itoa(myi)
 fmt.Println(mstr)

 // 字符串轉 float32 轉換 bool
 mFloat, error := strconv.ParseFloat("3.1415", 64)
 if error != nil {fmt.Println("convert error")
 }
 fmt.Println(mFloat)
 parseBol, err := strconv.ParseBool("true")
 if err != nil {fmt.Println("convert error")
 }
 fmt.Println(parseBol)
 // 基本類型轉字符串
 boolStr := strconv.FormatBool(true)
 fmt.Println(boolStr)

 floatStr := strconv.FormatFloat(3.1415, 'E', -1, 64)
 fmt.Println(floatStr)

運算符

go 語言提供哪些集合類型

package main

import "fmt"

func main() {
 // 數組 slice map list
 // 數組 var name [count]int
 //var course1 [3]string // course1 是類型 只有 3 個元素的數組類型
 //var course2 [4]string
 //course1[0] = "og"
 //course1[1] = "grpc"
 //course1[2] = "gin"
 ////[]strig 和 [3]string 這是兩種不同的類型
 //fmt.Println("%T \r\n", course1)
 //fmt.Println("%T \r\n", course2)
 //
 //for _, value := range course1 {// fmt.Printf("value=%s\n", value)
 //}

 // 初始化
 course1 := [3]string{"go", "grpc", "gin"}
 //course1 := [3]string{2:"gin"}
 // course3:=[...]string{"go","grpc","gin"}
 for _, value := range course1 {fmt.Printf("value=%s\n", value)
 }

 // 數組元素長度及內容一樣,可以直接用等於判斷
 // 多維數組
 var courseInfo [3][4]string
 courseInfo[0] = [4]string{"go", "1h", "bobby", "go 體系課 "}
 courseInfo[1] = [4]string{"grpc", "2h", "bobby1", "grpc 入門 "}
 courseInfo[2] = [4]string{"gin", "2h", "bobby2", "gin 高級開發 "}

 for i := 0; i < len(courseInfo); i++ {for j := 0; j < len(courseInfo[i]); j++ {fmt.Print(courseInfo[i][j] + "")
  }
  fmt.Println()}
}

切片

package main

import "fmt"

func main() {
 // 理解爲動態的 array 弱化數組的概念 切片的本質存儲和數組是有區別
 var courses []string
 fmt.Printf("%T \r\n", courses)

 // 這個方法很特別 添加元素
 courses = append(courses, "go")
 courses = append(courses, "grpc")
 courses = append(courses, "gin")

 fmt.Println(courses[1])

 // 初始化 3 種 1:從數組直接創建 2:使用 string{} 3:make
 allCourses := [5]string{"go", "grpc", "gin", "mysql", "elasticsearch"}
 courseSlice := allCourses[0:2] // 左閉右開的區間 python 的語法
 fmt.Println(courseSlice)

 courseSlice1 := []string{"go", "grpc", "gin", "mysql", "elasticsearch"}
 fmt.Println(courseSlice1)
 courseSlice2 := make([]string, 3)
 courseSlice2[0] = "C"
 fmt.Println(courseSlice2)

 // 如何訪問切片的元素 訪問單個(類似數組,不能超長度)訪問多個 allCourses[start:end] 前閉後開,如果只有 start(到結束)
 // 如果沒有 start 有 end(從 end 之前的元素)// 沒有【:] 相當於複製了一份
 cSlice1 := []string{"go", "grpc"}
 cSlice2 := []string{"mysql", "es", "gin"}
 c12 := append(cSlice1, cSlice2[1:]...)
 fmt.Println(c12)
}
package main

import (
 "fmt"
 "strconv"
 "unsafe"
)

func printSlice(data []string) {data[0] = "java"
 for i := 0; i < 10; i++ {data = append(data, strconv.Itoa(i))
 }
}

// 定義 slice 會通過結構體去定義
type slice struct {
 array unsafe.Pointer
 len   int
 cap   int
}

func main() {
 //go 的 slice 在函數 參數傳遞的時候是值傳遞還是引用傳遞,值傳遞,效果又呈現出引用的效果(不完全是)course := []string{"go", "grpc", "gin"}
 printSlice(course)
 fmt.Println(course)
}

defer 是有能力修改返回值的
error,panic,recover go 語言的錯誤處理理念, 一個函數可能出錯, 開發函數的人需要有一個返回值去告訴調用者是否成功,要求我們必須要處理這個 error
go 設計者認爲必須要處理這個 error, 防禦型編輯

func A() (int,error){panic("this is an panic") // panic 會導致程序的退出,平時開發中不要隨便使用,一般我人在哪裏用到,我們一個服務啓動的過程中 

  return 0,errors.New("this is an error")
}

結構體

 package main

type Person struct {
 name    string
 age     int
 address string
 height  float32
}

type Person1 struct {
 name string
 age  int
}

type Student struct {
 //// 第一種嵌套方式
 //p     Person1
 // 第二種方式 訪問的方式可以直接.name .age 但初始化時不能 類似覆蓋的方式
 Person1
 score float32
}

func main() {
 // 如何初始化結構體
 p1 := Person{"bobby1", 18, " 七星高照 ", 1.80}
 p2 := Person{name: "bobby2", height: 1.90}
 var persons []Person
 persons = append(persons, p1)
 persons = append(persons, p2)
 persons = append(persons, Person{name: "bobby3",})
 // 匿名結構體
 address := struct {
  province string
  city     string
  address  string
 }{
  province: " 北京市 ",
  city:     " 通州區 ",
  address:  " 萬壽路 ",
 }

 // 結構體的嵌套
 s := Student{
  p: Person1{
   name: "bob",
   age:  18,
  },
  score: 95.6,
 }
 s.p.name = "zzz"
}
// 結構體綁定方法
 // func(s StructType)funcName(param1 paramType,....)(returnTypes1....){.....}

指針

// 第一種初始化方式
ps := &Person{}

// 第二種初始化方式
var emptyPerson Person
pi := &emptyPerson

// 第三種初始化方式
var pp = new(Person)
fmt.Println(pp.name)

// 初始化兩個關鍵字,map、channel、slice 初始化推薦使用 make 方法
// 指針初始化推薦使用 new 函數,指針要初始化否則會出現 nil pointer
// map 必須初始化

指針傳遞交換兩個值

package main

import "fmt"

// 通過指針交換兩個值
func swap(a, b *int) {*a, *b = *b, *a}

func main() {
 x, y := 10, 20

 fmt.Printf(" 交換前: x = %d, y = %d\n", x, y)

 // 調用 swap 函數交換值
 swap(&x, &y)

 fmt.Printf(" 交換後: x = %d, y = %d\n", x, y)
}

go 語言中的 nil

/*
不同類型的數據零值不一樣

bool       false
numbers    0
string     ""
pointer    nil
slice      nil
map        nil
channel、interface、function nil

struct 默認值不是 nil,默認值是具體字段的默認值
*/
package main

import "fmt"

type Person struct {
    name string
    age  int
}

func main() {
  // struct 每一個值都等於才相當
    p1 := Person{
        name: "bobby",
        age:  18,
    }

    p2 := Person{
        name: "bobby",
        age:  18,
    }

    if p1 == p2 {fmt.Println("yes")
    }
}

go 鴨子

// Go 語言的接口,鴨子類型,php,python
// Go 語言中處處都是 interface,到處都是鴨子類型 duck typing

/*
當看到一隻鳥走起來像鴨子,游泳起來像鴨子,叫起來也像鴨子,那麼這隻鳥就是鴨子
動詞,方法,具備某些方法
*/
// 主要是聲明方法的定義
type Duck interface {
  // 方法的申請
  Gaga()
  Walk()
  Swimming()}
type pskDuck struct {legs int}
func (pd *pskDuck) Gaga(){fmt.Println(" 嘎嘎 ")
}
func (pd *pskDuck) Walk(){fmt.Println(" 嘎嘎 ")
}
func (pd *pskDuck) Swimming (){fmt.Println(" 嘎嘎 ")
}

go 語言的包組織

同一個文件夾下不允許出現不同名的 pacage, 導入是使用路徑,使用時候是包名 + 類包,別名引用;.全部引入到當前包中使用, 初始化的時候會使用 func init(){}, go.mod 是自動維護的gin-gonic/gin
新版都是使用 go modules go list -m all

go list -m --version 包名 可以查看版本
go get 指定包名
go mod tidy # go mod help 來查看
# go install 安裝
# go get -u 升級到最新的將要版本或修定版本
# # go get 會修改 go.mod 文件的
# go get github.com/go-redis/redis/v8@version 

代碼規範

1. 代碼規範

命名

  • 使用駝峯命名法,例如:myVariable
  • 包名應爲小寫單詞,無需使用下劃線或混合大小寫。
  • 接口命名以 -er 結尾,例如:ReaderWriter

格式化

  • 使用 gofmt 工具統一代碼格式。
  • 保持代碼風格一致,如縮進、空格和換行等。

註釋

  • 使用行註釋 // 爲函數、方法和複雜邏輯添加說明。
  • 包級註釋應放在包聲明之前。

2. 結構體與接口

結構體

  • 儘量使用小寫字段名,除非需要導出。
  • 使用構造函數初始化結構體。

接口

  • 定義小而簡單的接口。
  • 使用接口滿足需要,而不是定義大而全的接口。

3. 錯誤處理

錯誤檢查

  • 始終檢查函數返回的錯誤。
  • 使用 errors.Newfmt.Errorf 創建錯誤信息。

錯誤處理

  • 錯誤應儘早處理,避免延遲檢查。
  • 當錯誤不可恢復時,使用 log.Fatal 記錄日誌並退出。
  • 儘量提供上下文信息以幫助調試。

爲什麼要代碼規範

  1. 代碼規範並不是強制的,但是不同的語言一些細微的規範還是要遵循的。
  2. 代碼規範主要是爲方便團隊內形成一個統一的代碼風格,提高代碼的可讀性、統一性。

1. 代碼規範

1.1 命名規範

包名

  1. 儘量和目錄保持一致。
  2. 儘量採取有意義的包名,簡短。
  3. 不要和標準庫名衝突。
  4. 包名採用全部小寫。

文件名

  • 使用 user_name.go,如果有多個單詞可以採用蛇形命名法。

變量名

  1. 蛇形:適用於 python、php。
  2. 駝峯:適用於 java、c、go。
  3. userName
  4. UserName
  5. un string //unad userNameAndDesc

  6. 有一些專有命名,如 URLVersion

  7. bool 類型使用 Hasiscanallow 等前綴。

1.2 結構體命名

  • 使用駝峯,例如 User

1.3 接口命名

  • 接口命令基本上和結構體差不多。

單元測試

多線程

// python, java, php 多線程編程、多進程編程,多線程和多進程存在的問題主要是耗費內存
// 內存、線程切換、web2.0,用戶級線程,綠程,輕量級線程,協程,asyncio-python php-swoole java - netty
// 內存佔用小(2k)、切換快,go 語言的協程,go 語言誕生之後就只有協程可用 goroutine
// 主協程退出,在其中啓動的協程就會死掉
package main

import (
 "fmt"
 "time"
)

func asyncPrint() {fmt.Println("bobby")
}

func main() {
 // 主死隨從
 //go asyncPrint()
 // 1. 閉包 2. for 循環的問題
 for i := 0; i < 100; i++ {go func(i int) {
   for {time.Sleep(time.Second)
    fmt.Println(i)
   }
  }(i)
 }

 fmt.Println("main goroutine")
 time.Sleep(2 * time.Second)
}

理解 GMP

wg (wait group)

雖然我們可以使用主線程 sleeep 來保證主協程不死,但主不一定知道要 sleep 要多久,子 goroutine 如何通知到主的 goroutine 自己結束了,主的 goroutine 如何知道子的已經結束了

package main

import (
 "fmt"
 "sync"
)

func main() {
 var wg sync.WaitGroup

 // 我要監控多少個 goroutine 執行結束
 wg.Add(100)
 for i := 0; i < 100; i++ {go func(i int) {defer wg.Done()
   fmt.Println(i)
   //wg.Done() // 調用了要調用一次 done}(i)
 }
 // 等到 100 都執行完
 wg.Wait()

 fmt.Println("all done")
 // waitgroup 主要用於 goroutine 的執行等待 add 方法要和 done 要和 Done 方法配套
}

goroutine 如何使用鎖

package main

import (
 "fmt"
 "sync"
)

// 鎖 資源競爭

var total int
var wg sync.WaitGroup
var lock sync.Mutex // 只要是同一把鎖就沒有問題

func add() {defer wg.Done()
 for i := 0; i < 100000; i++ {lock.Lock()
  total += 1
  lock.Unlock()}
}

func sub() {defer wg.Done()
 for i := 0; i < 100000; i++ {lock.Lock()
  total -= 1
  lock.Unlock()}
}

func main() {wg.Add(2)
 // 兩個同時運行時,因爲不是原子操作它會產生資源競爭,導致結果不一致
 go add()
 go sub()
 wg.Wait()

 fmt.Println("done i=", total)
}
// 如果權權是加一或者減一操作
// 可以使用 atomic.AddInXX

讀寫鎖

上面我們已經學習了互斥(本質上是將並行的內容,串行化),使用 lock 肯定會影響性能
即使是設計鎖,那麼也要儘量保證並行
我們有兩組協程,一組寫,一組讀,web 系統中絕大數是讀多寫少,比如詳情頁面
雖然有多個 goroutine, 但仔細分析我們會發現 讀協程之間應該是併發,讀和寫之間應該是串行

package main

import (
 "fmt"
 "sync"
 "time"
)

func main() {
 var num int
 var rwLocak sync.RWMutex
 var wg sync.WaitGroup

 wg.Add(2)

 go func() {defer wg.Done()
  // 負責寫數據
  rwLocak.Lock() // 加寫鎖 寫鎖會防止 別的寫鎖獲取和讀鎖獲取
  defer rwLocak.Unlock()
  num = 12
 }()

 // 因爲不能保證 寫先執行(因爲還沒講到 goroutine 之間的通信)我們先 sleep 一下吧
 time.Sleep(time.Second)
 // 讀取的 goroutine
 go func() {defer wg.Done()
  rwLocak.RLock() // 加讀鎖,讀鎖不會阻止別人的讀
  defer rwLocak.RUnlock()
  fmt.Println(num)
 }()
 wg.Wait()}

goroutine 之間進行通信

package main

import "fmt"

func main() {
 /*
    不要通過共享內存來通信,而要通過通信來實現內存共享
    php python java 多線程編程的時候,丙從此 goroutine 之間通信最常用的方式是一個全局可以提供消息的機制
    python-queue java 生產者消費者
    channel 在加上語法糖讓使用 channel 更加簡單
 */
 var msg chan string // 可以理解是一個通道 還要定義通道的類型
 // 它底層是數組來完成,所以要初始一下
 msg = make(chan string, 1) // channel 的初始化值爲 0 時,你放的值會阻塞 deadlock
 // msg = make(chan string, 0)  // 無緩衝
 msg <- "bobby" // 放值 右邊的值放到這個 channel 中
 data := <-msg  // 拿值
 fmt.Println(data)
}

有緩衝和緩衝

  • 無緩衝 適用於通知 B 要第一時間知道 A 是否已經完成
  • 有緩衝 適用於生產者之間通信
// 消息傳遞,消息過濾
// 信號廣播
// 事件訂閱和廣播
// 任務分發
// 結果彙總
// 併發控制
// 同步和異步
package main

import (
 "fmt"
 "time"
)

func main() {
 var msg chan int
 msg = make(chan int, 2)
 go func(msg chan int) {
  for data := range msg {fmt.Println(data)
  }
  fmt.Println("all done")
 }(msg)

 msg <- 1 // 放值 channel 中
 msg <- 2
 close(msg) // 關閉這個通道
 d := <-msg
 fmt.Println(d) // 已經關閉的 channel 可以繼續取值,但不能再放值了
 msg <- 3       // 已經關閉的 channel 不能再放值了

 time.Sleep(time.Second * 10)

}

單向 channel

package main

import (
 "fmt"
 "time"
)

func producer(out chan<- int) {
 for i := 0; i < 10; i++ {out <- i * i}
 close(out)
}

func consumer(in <-chan int) {
 for num := range in {fmt.Printf("num=%d\r\n", num)
 }
}

func main() {
 // 默認情況下,channel 是雙向的
 // 但是我們經常一個 channel 做爲一個參數時,不希望往裏寫數據
 // 單向 channel
 //var ch1 chan int       // 雙向的
 //var ch2 chan<- float64 // 單向的只能寫入 float64
 //var ch3 <-chan int     // 單向的,只能讀取數據

 //c := make(chan int, 3)
 //var send chan<- int = c // send-only
 //var read <-chan int = c // rec-oney
 //send <- 1
 //<-send // 這樣就不行了,只能寫不能讀
 //<-read
 // 不能將單向的轉成普通的 channel
 c := make(chan int)
 go producer(c)

 go consumer(c)

 time.Sleep(time.Second * 10)

}

一道常見面試題

package main

import (
 "fmt"
 "time"
)

var number, latter = make(chan bool), make(chan bool)

func printNum() {
 i := 1
 for {
  // 我怎麼去做到,應該此片,等待另一個 goroutine 來通知我
  <-number
  fmt.Printf("%d%d", i, i+1)
  i += 2
  latter <- true
 }
}

func printLetter() {
 i := 0
 str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
 for {
  // 我怎麼去做到,應該此片,等待另一個 goroutine 來通知我
  <-latter
  if i >= len(str) {return}
  fmt.Print(str[i : i+2])
  i += 2
  number <- true
 }
}

func main() {
 /*
  使用兩個 goroutine 交替打印序列,一個 goroutine 打印數字,另外一個 goroutine 打印字母,最終效果如下:12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728
 */
 go printNum()
 go printLetter()
 number <- true
 time.Sleep(time.Second * 100)
}

監控

package main

import (
 "fmt"
 "sync"
 "time"
)

var done bool
var lock sync.Mutex

func go1() {time.Sleep(time.Second)
 lock.Lock()
 defer lock.Unlock()
 done = true
}

func go2() {time.Sleep(time.Second * 2)
 lock.Lock()
 defer lock.Unlock()
 done = true
}

func main() {
 // 類似於 switch 但 select 的功能和我們操作系統 linux 裏面提供的 io 的 select poll epoll 類似
 // select 主要作用於多個 channel
 // 現在有需求,我們現在有兩個 goroutine 都在執行,但是我們在主的 goroutine 中,當某一個執行完成了,這個時間我會立以知道
 go go1()
 go go2()
 for {time.Sleep(time.Millisecond * 10)
  if done {fmt.Println("done")
   return
  }
 }
}

更推崇消息的方式來通知,而不是共享變量的方式

context

併發編程中使用最多一個場景了

package main

import (
 "fmt"
 "sync"
 "time"
)

var wg sync.WaitGroup
var stop bool

// 我們新的需求,我可以主動退出監控程序
// 共享變量
func cupInfo() {defer wg.Done()
 for {
  if stop {break}
  time.Sleep(2 * time.Second)
  fmt.Println("CPU 的信息 ")
 }
}

func main() {
 // 爲什麼使用 context
 // 有一個 goroutine 監控 cpu 的信息
 wg.Add(1)
 go cupInfo()
 time.Sleep(6 * time.Second)
 stop = true
 wg.Wait()
 fmt.Println(" 監控完成 ")
}
// 演進
package main

import (
 "context"
 "fmt"
 "sync"
 "time"
)

var wg sync.WaitGroup

// var stop bool
//var stop = make(chan struct{})

// 我們新的需求,我可以主動退出監控程序
// 共享變量
func cupInfo(ctx context.Context) {defer wg.Done()
 for {
  select {case <-ctx.Done():
   fmt.Println(" 退出 cpu 監控 ")
   return
  default:
   time.Sleep(2 * time.Second)
   fmt.Println("CPU 的信息 ")
  }

 }
}

func main() {
 // 爲什麼使用 context
 // 有一個 goroutine 監控 cpu 的信息
 //var stop = make(chan struct{})
 wg.Add(1)
 ctx, cancel := context.WithCancel(context.Background())
 go cupInfo(ctx)
 time.Sleep(6 * time.Second)
 //stop <- struct{}{}
 cancel()
 wg.Wait()
 fmt.Println(" 監控完成 ")
}
package main

import (
 "context"
 "fmt"
 "sync"
 "time"
)

var wg sync.WaitGroup

// var stop bool
//var stop = make(chan struct{})

// 我們新的需求,我可以主動退出監控程序
// 共享變量
func cupInfo(ctx context.Context) {defer wg.Done()
 for {
  select {case <-ctx.Done():
   fmt.Println(" 退出 cpu 監控 ")
   return
  default:
   time.Sleep(2 * time.Second)
   fmt.Println("CPU 的信息 ")
  }

 }
}

func main() {
 // 爲什麼使用 context
 // 有一個 goroutine 監控 cpu 的信息
 //var stop = make(chan struct{})
 wg.Add(1)
 //ctx, cancel := context.WithCancel(context.Background())
 ctx, _ := context.WithTimeout(context.Background(), 6*time.Second)
 go cupInfo(ctx)
 //time.Sleep(6 * time.Second)
 //stop <- struct{}{}
 //cancel()
 wg.Wait()
 fmt.Println(" 監控完成 ")
}
正文完
 0