轉型
- 想在短時間系統轉到 Go 工程理由
- 提高 CRUD,無自研框架經驗
- 拔高技術深度,做專、做精需求的同學
- 進階工程化,擁有良好開發規範和管理能力的
工程化的重要性
高級開的期望
- 良好的代碼規範
- 深入底層原理
- 熟悉架構
- 熟悉 k8s 的基礎架構
擴展知識廣度,知識的深度,規範的開發體系
四個大的階段
- go 語言基礎
- 微服務開發的(電商項目實戰)
- 自研微服務
- 自研然後重構
領域
- Web 開發 -gin、beego 等
- 容器虛擬化 -docker、k8s、istio
- 中間件 - etcd、tidb、influxdb、nsq 等
- 區塊鏈 以太坊、fabric
- 1xAR5 – go-zero, dapr, rpcx, kratos. dubbo-go,
相關環境的開發
- go 安裝 官網
- 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結尾,例如:Reader、Writer。
格式化
- 使用
gofmt工具統一代碼格式。 - 保持代碼風格一致,如縮進、空格和換行等。
註釋
- 使用行註釋
//爲函數、方法和複雜邏輯添加說明。 - 包級註釋應放在包聲明之前。
2. 結構體與接口
結構體
- 儘量使用小寫字段名,除非需要導出。
- 使用構造函數初始化結構體。
接口
- 定義小而簡單的接口。
- 使用接口滿足需要,而不是定義大而全的接口。
3. 錯誤處理
錯誤檢查
- 始終檢查函數返回的錯誤。
- 使用
errors.New或fmt.Errorf創建錯誤信息。
錯誤處理
- 錯誤應儘早處理,避免延遲檢查。
- 當錯誤不可恢復時,使用
log.Fatal記錄日誌並退出。 - 儘量提供上下文信息以幫助調試。
爲什麼要代碼規範
- 代碼規範並不是強制的,但是不同的語言一些細微的規範還是要遵循的。
- 代碼規範主要是爲方便團隊內形成一個統一的代碼風格,提高代碼的可讀性、統一性。
1. 代碼規範
1.1 命名規範
包名
- 儘量和目錄保持一致。
- 儘量採取有意義的包名,簡短。
- 不要和標準庫名衝突。
- 包名採用全部小寫。
文件名
- 使用
user_name.go,如果有多個單詞可以採用蛇形命名法。
變量名
- 蛇形:適用於 python、php。
- 駝峯:適用於 java、c、go。
userNameUserName-
un string //unad userNameAndDesc -
有一些專有命名,如
URLVersion。 bool類型使用Has、is、can、allow等前綴。
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(" 監控完成 ")
}