转型
- 想在短时间系统转到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("监控完成")
}
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://joyjs.cn/archives/4768