目录
概念
基础语法
并发编程
项目相关
报错案例
实战项目
概念
1.Go语言最主要的特性
- 自动垃圾回收
- 更丰富的内置类型
- 函数多返回值
- 错误处理
- 匿名函数和闭包
- 类型和接口
- 并发编程
- 反射
- 语言交互性
2.Go语言很可能是第一个将代码风格强制统一的语言,
- Go语言要求public的变量必须以大写字母开头,private变量则以小写字母开头,这种做法不仅免除了public、private关键字,更重要的是统一了命名风格
- Go语言对{}应该怎么写进行了强制,比如以下风格是正确的:而以下风格是错误的:
1
2
3if expression{
...
}1
2
3
4if expression
{
...
}
3.xorm是一个简单而强大的Go语言ORM库,经过它可使数据库操做很是简便。xorm的目标并非让你彻底不去学习SQL,咱们认为SQL并不会为ORM所替代,可是ORM将能够解决绝大部分的简单SQL需求。xorm支持两种风格的混用
- orm是一种封装数据库操作的框架,使用它可以简化sql操作
4.Go的内存回收整体阶段:
- Goff to Gmark,每次的gcstart都是满足gc_triger时由mallocgc触发,整个的启动过程是stop the world的,这个过程启动了所有的GC工作协程,进入GCMark状态使能写屏障,启动gcController。简单来说就是确定GCroot相关的goroutine。
- Gmark,这个阶段是标记阶段,拿到准备好的goroutine来做标记,但是一开始就gopark当前的goroutine(上个阶段),直到被gccontroller的findRunnableGCWorker唤醒。
唤醒后进入标记阶段,每个worker都去gc-work中拿节点(节点置黑),然后处理当前节点看有没有指针和没标记的对象,继续入队子节点(灰化节点),直到队列为空。 - Gmarktermination,标记结束后调用gcMarkDone
- Gsweep,具体的清除行为,有多个时机可以出发Gsweep,如果是并发清除的话,需要先回收未被标记的heap区,然后唤醒进行sweep的 goroutine。
3.main()函数不能带参数,也不能有返回值
4.go get -u xx的-u含义是,本地已经有一个名为xx的第三方库了,如果直接执行go get xx的话会什么都不执行,因为编译器会识别本地已经安装了所以直接跳过,而加上-u则会去拉取xx的最新版本
5.Go相比其他语言的优势
- 直接编译成二进制(和c/c++一样),没有虚拟化损失
- 一次编码适用于多种平台(和java一样)
- 超强并发支持能力与并发易用性
基础语法
1.字符串修改
1 | s2 := "白萝卜" |
2.for循环
1 | for _, c := range s{ |
3.定义数组
1 | var a [3]string = [3]string{"nihao", "jhj", "ir"} |
4.多维切片
1 | # 声明一个二维整型切片并赋值 |
5.多重返回值
- 定义
1
2
3
4
5
6
7
8
9
10
11
12func getName()(firstName, middleName, lastName, nickName string){
return "May", "M", "Chen", "Babe"
}
# 与上方等价
func getName()(firstName, middleName, lastName, nickName string){
firstName = "May"
middleName = "M"
lastName = "Chen"
nickName = "Babe"
return
} - 调用
1
2
3
4fn, mn, ln, nn := getName()
# 只需要lastName就用这个形式
_, _, ln, _ := getName()
6.=是赋值,:=是声明变量并赋值,注意函数外和const常量都必须用var xx的形式,不能用:=
1 | //使用=赋值之前必须使用先var声明例如: |
7.函数的写法
1 | func test(x, y int, s string) (int, string) { |
- 使用关键字func定义函数
- 参数类型要在参数名后面
- 当两个或多个连续的函数命名参数是同一类型,则只需写最后一个参数的参数类型
- 多返回值必须用括号,单返回值可以不用,如果没有返回值的话可以直接不写,注意只有没有返回值的情况下才能不写返回值类型
8.按地址传递的函数写法
- 定义
1
2
3
4
5
6func swap(x, y *int) {
var temp int
temp = *a
*a = *b
*b = temp
} - 使用
1
2
3a := 10
b := 1
swap(&a, &b) - 即不可以像c++一样函数只管用引用格式接受就行了,而是main()里面传地址然后函数用指针形式接受
9.nil是引用类型的默认值
10.常用占位符
- %v:变量的默认格式
- %p:变量的地址,十六进制表示,前缀0x
- %d:变量的十进制表示
11.for循环的写法
- C++风格写法
1
2
3for i := 0; i < 10; i++ {
sum += i
} - py风格写法
1
2
3
4
5
6var temple = []string{token, timestamp, nonce}
var codeString string
for _, v := range temple { //_是下标,v是元素
codeString += v
}
//最终codeString是token+timestamp+nonce
12.if else写法
1 | if codeString == signature { |
13.(函数也是一模一样)在Go中名字是以大写字母开头的表示该变量就是已导出的,可以在main中用包名.变量名访问,否则不行
- 大写的变量对于同一个包的其他go文件而言也是可见的,即使这个大写变量是在局部定义的,它也相当于一个全局变量
14.类型转换示例,Go不支持不同类型隐式转换
- int转string,strconv.Itoa
1
func Itoa(i int) string
- string转int,strconv.Atoi
1
func Atoi(s string) (int, error)
- 数值类型转换
1
2
3
4d1 := 24.1
i := int(d1)
f := float(i)
u := uint(f) - 字符串转time类型
1
2
3
4tmp, _ := time.LoadLocation("Asia/Shanghai") //加时区
tmpTime, err := time.ParseInLocation("2006-01-02 15:04:05", tmpStr, tmp)
//tmpStr是string类型,tmpTime是time类型 - time类型转时间戳
1
2//startTime是time类型,startUnix是int64类型的时间戳
startUnix := startTime.Unix() - 时间戳转time类型
1
2//upInt是int64类型的时间戳,uptime是time类型
upTime := time.Unix(upInt, 0)
15.切片
- 切片并不存储任何数据,切片就像数组的引用,修改切片会影响数组
- 下列代码表示b包含a中下标为1、2、3的元素
1
b := a[1:4]
- 切片下界默认值为0,上界默认值是切片对象的总长度
- 切片和数组的重要区别
1
2
3s := []int{2, 3, 5, 7, 11, 13} //此时的s是数组,长度固定,只能另外定义一个切片来对其操作
s := []int{2, 3, 5, 7, 11, 13} //此时的s是切片,即后续完全可以用s=s[2:]这样来改变长度 - 切片的长度lens()是该切片实际含有的元素数目,容量caps()是该切片的第一个元素到底层数组的末尾元素之间的元素数目
- 切片的零值是nil
- 声明一个空切片
1
var s []int
- 用make创建切片
1
2a := make([]int, 5, 10) //a是一个长度为5元素值为0的int数组的切片,预设cap是10
- 二维数组用切片的切片来实现
- 往切片中追加元素
1
2
3
4
5var temple []int
for i := 0; i < 10; i++ {
fmt.Println(i)
temple = append(temple, i*10)
}
16.映射
- 定义
1
var m map[键类型]值类型
- 使用
1
fmt.Println(m[键名1])
- 删除某个键值对
1
delete(m, 键名1)
- 检测某个键是否存在
1
2
3v, ok := m[键名1]
//若存在,则v是值,ok为true
//若不存在,则v是0,ok为false
17.闭包
- Go函数可以是一个闭包,闭包是一个函数值,它引用了其函数体之外的变量
- 示例,函数adder返回一个闭包,每个闭包都被绑定在其各自的sum 变量上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
18.go中没有类,只有结构体所以结构体方法要如下
- 定义
1
2
3
4
5
6
7type Circle struct {
radius float64
}
func (c Circle) getArea() float64 { //(c Circle)表示该方法是Circle的方法
return 3.14 * c.radius * c.radius //c.radius 即为 Circle 类型对象中的属性
} - 使用
1
2
3
4
5func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println( c1.getArea())
}
19.批量定义常量用如下形式
1 | const ( |
20.反射
- 结构体使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17type OrderList struct {
Id int `json:"id"` //json:"id"就是tag
OddNum string `json:"odd_num"` //json:"odd_num"就是tag
}
//声明多个结构体要在type后加括号
type(
OrderList1 struct {
Id int `json:"id"` //json:"id"就是tag
OddNum string `json:"odd_num"` //json:"odd_num"就是tag
}
OrderList2 struct {
Id int `json:"id"` //json:"id"就是tag
OddNum string `json:"odd_num"` //json:"odd_num"就是tag
}
)
21.panic的用法
- 调用panic()后会打印里面的信息,然后终止程序运行
1
panic()
22.error也可以作为返回类型,nil可以作为它的值
23.interface{} 就是一个空接口,所有类型都实现了这个接口,所以它可以代表所有类型,所以使用空接口实现可以保存任意值的字典。
- 使用例子
1
2
3
4data := map[string]interface{} {
"cal_detail" : calDetail,
"cal_adjust_date":calAdjust.Date,
} - 一个带有switch的经典案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18func main() {
m := make(map[string]interface{})
m["int"] = 123
m["string"] = "hello"
m["bool"] = true
for _, v := range m {
switch v.(type) {
case string:
fmt.Println(v, "is string")
case int:
fmt.Println(v, "is int")
default:
fmt.Println(v, "is other")
}
}
fmt.Println(m)
} - 将interface{}转换成string或int类型
1
2
3
4//a是interface{}类型
str_a := a.(string)
int_a := a.(int)
24.用断言判断类型,主要用于判断interface{}类型
1 | value, ok = x.(T) //若x是T类型,则value为x的值,ok为true,否则ok为false,value仍为x的值 |
25.Go的字符串操作
- 截取字符串
1
s2 := s1[start:end] //若start为空则默认为0,若end为空则默认为s1.len()-1
- 找到某个字符的最后出现下标
1
index := strings.LastIndex("/", dirName)
- 判断s是否包含子字符串substr
1
flag := strings.Contains(s, substr)
- 返回s中子字符串substr出现的下标,没有则返回-1(Contains方法就是调用Index实现的)
1
index := strings.Index(s, substr)
- 如果后缀是”\r\n”那么就去掉后缀
1
str = strings.TrimSuffix(str, "\r\n")
26.对字符串用特定字符串分割
1 | str := "1223-6" |
27.map的一些使用事项
- 必须用memoryOne := map[string]bool{}这样初始化声明后,才能用memoryOne[“001”] = false来添加值,不能仅仅只是var
- 判断key是否存在
1
2
3_, ok := memoryOne["001"]
# 存在的话ok会是true,不存在的话ok会是false
28.因为go可以实现多重赋值,所以交换i,j的功能可以由以下一行代码实现
1 | i, j = j, i |
29.const声明的常量可以不加类型,且常量右边应该是编译期就能获取的值(运算符里只有>>和<<)
30.atoi是一个自增常量,在遇到下一个const的时候会自动置0,不然每出现一次下一次它的值就为+1
31.go的bool类型不支持自动类型转换,必须是一个逻辑值
32.float32等价于c的float,float64等价于c的double
33.不能直接用str[1]=’x’这样的方式去修改一个string的值
- 以byte遍历,byte的本质就是一个8位的二进制数字,String转byte[],就是先转ASCII码,然后再转成二进制
1
2
3for i := 0; i < len(str); i++ {
ch := str[i] // 依据下标取字符串中的字符,类型为byte fmt.Println(i, ch)
} - 以rune遍历
1
2
3for i, ch := range str {
fmt.Println(i, ch) //ch的类型为rune
}
34.往一个切片末尾添加另一个切片所有元素的方法
1 | mySlice = append(mySlice, mySlice2...) //注意如果第二个参数是切片而不是值的话,一定要加上... |
35.基于老切片生成新切片时,可以超过老切片的len,只要不超过老切片的cap就不会报错
36.goto跳转语句
- 用xx:声明
- 用goto xx跳转
37.不定参数,只能在函数参数列表里出现,并且要是最后一个参数(下面的args实际类型时[]int)
1 | func main() { |
38.切片就相当于vector,基于数组生成的切片其实是把数组复制了一份,更改切片的值并不影响数组
- 切片同vector原理一样也有指针、长度len、容量cap(用make初始化的时候cap=len)
- 用var temple []int = []int{1,2}这样没指定长度生成的数组也是切片
- 切片在传参进函数的时候,默认是按引用传递
39.defer语句的调用是遵照先进后出的原则,即最后一个defer语句将最先被执行
39.匿名函数由一个不带函数名的函数声明和函数体组成
1 | func(a, b int, z float64) bool { |
40.如何在运行时检查变量类型
- 类型开关(Type Switch)是在运行时检查变量类型的最佳方式。类型开关按类型而不是值来评估变量。每个Switch 至少包含一个case用作条件语句,如果没有一个case 为真,则执行default
41.cap()函数的适用范围
- 数组
- 切片
- 带缓冲的channel
42.Go中的new是将一个该类型的空间置零,返回值是指针类型
- 即在new的时候就已经分配内存了,不用等到定义时才分配内存
1
c := new(int)
43.三个打印函数
- Printf()是标准输出,一般是屏幕,也可以重定向。
- Sprintf()是把格式化字符串输出到指定的字符串中。
- Fprintf()是把格式化字符串输出到文件中
44.slice的底层原理
- 切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据
- 扩容的时候也是和vector一样,重新开辟内存然后把旧值复制过来再销毁旧内存
- 扩容策略
- 首先判断,如果新申请容量大于2倍的旧容量,最终容量就是新申请的容量
- 否则判断,如果旧切片的长度小于1024,则最终容量就是旧容量的两倍
- 否则判断,如果旧切片长度大于等于1024,则最终容量从旧容量开始循环增加原来的1/4, 直到最终容量大于等于新申请的容量
- 如果最终容量计算值溢出,则最终容量就是新申请容量
45.go中默认按值传递和按地址传递的类型
- 按值传递:int、string、struct(属于非引用类型),在函数中就无法修改原内容数据
- 按地址传递:指针、map、slice、chan(属于引用类型),在函数中可以修改原内容数据
46.map的底层原理
- Golang中map的底层实现是一个散列表,因此实现map的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫hmap,一个叫bmap,通常叫其bucket。
- 扩容机制
- 双倍扩容:扩容采取了一种称为“渐进式”的方式,原有的key并不会一次性搬迁完毕,每次最多只会搬迁2 个bucket
- 等量扩容:重新排列,极端情况下,重新排列也解决不了,map存储就会蜕变成链表,性能大大降低,此时哈希因子hash0的设置,可以降低此类极端场景的发生
- 查找
- Go语言中map采用的是哈希查找表,由一个key通过哈希函数得到哈希值,64位系统中就生成一个64bit的哈希值,由这个哈希值将key对应存到不同的桶(bucket)中,当有多个哈希映射到相同的的桶中时,使用链表解决哈希冲突
- 细节:key经过hash后共64位,根据hmap中B的值,计算它到底要落在哪个桶时,桶的数量为2^B,如B=5,那么用64位最后5位表示第几号桶,在用hash值的高8位确定在bucket中的存储位置,当前bmap中的bucket未找到,则查询对应的overflow bucket,对应位置有数据则对比完整的哈希值,确定是否是要查找的数据。如果当前map处于数据搬移状态,则优先从老buckets查找。
46.slice类型
- 本质是对底层数组的一段内容的引用
- 一个slice包含数组、长度(len)、容量(cap)三个元素
- 两个slice类型,如果用=的话,那么就会共用底层数组。不想共用的话就要用make分别声明后,再用copy(目标, 原数据)来拷贝数据
47.channel
- 一个channel包含长度和类型两个元素
- 读 v:=<-ch
- 写 ch<-v
- 关闭 close(ch)
- 长度为0的表示不带buffer的channel(读优先于写),不为0表示带buffer的channel(写优先于读,存在数据拷贝的操作)
48.interface
- 一个interface没有值,它只能用来确认实现了哪些方法,然后这些方法的定义也不包含
- 实质上就是一个n个接口声明放在一起的结构
49.go的context是用于并发用途的,它本质也是一个接口
50.http/net等标准库都是服务端接受到一个请求时,都会新建一个协程去执行它
51.go成员函数可以有以下两种写法
- 值
1
2
3func (u user) xx(){
}
//这种对值的修改就只能局限于在user这个结构体里面 - 指针
1
2
3func (u *user) xx(){
}
//这种对值的修改就没有限制
52.以下这种类型转换其实就是断言
1 | var b = a.(int) |
- 以下这种方式会返回a的类型名
1
string str = a.(type)
测试
- Go Convey 是什么?一般用来做什么?
- 是一个支持Golang的单元测试框架
- 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到Web界面
- 提供了丰富的断言简化测试用例的编写
一些库的使用
go-commons-pool库,用于实现连接池
- 引入
1
2//在你的结构体内定义这个成员
peerConnection *pool.ObjectPool - 实现
- 新建一个cluster_pool.go文件
- 里面写一个factory类去实现pool.PooledObject接口,实现MakeObject和DestroyObject方法即可
- 操作
- 定义
1
xx.peerConnection, err = MakeObject(xx)
- 从连接池里拿一个连接
1
object := pool.BorrowObject(context.Background())
- 将拿的连接还回去
1
pool.returnObject(context.Background(), client)
- 定义
bufio库
1.从buffer里面读一行以’\n’为换行表示的[]byte
1 | var bufReader *bufio.Reader |
2.从buffer里面取内容,直到填满msg
1 | msg = make([]byte, 2) |
http库
1.应用demo
- 发送Get请求(接受一个返回的json)
1
2
3
4
5
6
7
8type responseGet struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
var getRes responseGet
responseGet, _ := http.Get(urlGet) //此处responseGet是*http.response类型
json.NewDecoder(responseGet.Body).Decode(&getRes) //用结构体getRes来对应存Get返回的数据 - 发送带json的Post请求
1
2
3
4
5
6
7
8
9
10
11
12data := make(map[string]interface{})
data["path"] = request.Path
data["scene"] = "a"
bytesData, _ := json.Marshal(data) //构造成json格式
reader := bytes.NewReader(bytesData) //构造新reader对象
requestPost, err := http.NewRequest("POST", urlPost, reader) //构造request对象,此处requestPost是*http.Request类型
requestPost.Header.Set("Content-Type", "application/json;charset=UTF-8") //设置请求头
client := http.Client{} //设置client为发起请求的客户端
resp, err := client.Do(requestPost) //发起请求,此处resp是*http.response类型
respBytes, err := ioutil.ReadAll(resp.Body) //将响应的数据读出来,respBytes是二进制数组
2.*http.request的数据处理
- 新增cookie
1
2cookieToken := http.Cookie{Name:"token", Value:token, Path:"/"}
req.AddCookie(cookieToken) - 读取cookie
1
token, err := req.Cookie("token")
- 从header里取数据
1
2# req是*http.Request类型
token := req.Header.Get("Authorization") - get请求里取传入的参数
1
param, err := url.ParseQuery(req.URL.RawQuery)
3.http接口的写法
1 | func main(){ |
5.*http.response的读数据方式
- 常规处理
1
2
3
4# resp是*http.response类型
respBytes, err := ioutil.ReadAll(resp.Body)
respString = string(respBytes)
fmt.Println(respString) - 直接通过json转成结构体实例
1
2# resp是*http.response类型,getRes是满足json格式要求的结构体的实例
json.NewDecoder(resp.Body).Decode(&getRes)
time库
1.time.sleep()默认单位是纳秒,即1000,000,000才等于休眠1秒
2.time.time类型的变量x可以用x.Format(“2006-01-02 15:04:05”)来转换成符合格式的字符串
1 | # time1是值为2022-01-02 15:04:05的time.time类型变量 |
3.time类型进行加减
1 | h, _ := time.ParseDuration("+1h") |
4.time类型进行比较
1 | time1.Before(time2) //如果time1早于time2返回true,否则返回false |
5.time.time类型的加减
1 | rightTime := data.Add(time.Duration(10)*time.Second) |
rand库
1.生成随机数
- rand.Intn(n)和rand.Int() % n都会生成一个0到n-1的随机int数
- rand.perm(n)会生成一个以0到n-1为元素的随机排序列表
2.用时间作随机数种子时,需要休眠1纳秒来避免程序运行过快导致的重复
1 | time.Sleep(1) |
3.打乱一个数组
1 | rand.Shuffle(len(arr), func(i, j int) { |
base64库
1.如何将二进制数组转换成base64表示的字符串(一般用于图片和文件)
1 | sEnc := base64.StdEncoding.EncodeToString(respBytes) |
- 解码base64
1
sDec, _ := b64.StdEncoding.DecodeString(sEnc)
sort库
1.将一个[]xx数组进行排序步骤
- 将数组声明成一个类型
1
type tar []xx
- 为这个类型写三个子方法Len(),Less(),Swap()
1
2
3
4
5
6
7
8
9
10
11
12func (t tar) Len() int{
return len(tar)
}
func (t tar) Less(i int, j int) bool{
return t[i] < t[j]
}
func (t tar) Swap(i int, j int) {
t[i], t[j] = t[j], t[i]
return
} - 使用排序
1
2var element tar
sort.Sort(element)
2.简单排序用法
1 | var intervals = [][]float64{{999.0,1},{726.1,3},{1000,2.2}} |
3.内置的sort函数
- sort.Ints([]int)
- 快速查找,下面demo为找到第一个大于等于target的下标
1
2
3
4target := 2
idx := sort.Search(len(m.nodeHashes), func(i int) bool {
return m.nodeHashes[i] >= target
})
log库
- 在日志里打印err信息并终止程序
1
2
3if err != nil{
log.Fatal("Err:", err)
}
sync库
1.sync.map的使用
- range(),需要传进去一个匿名函数,用来对每一个k-v施加操作,注意这个遍历开始的位置都是随机的
1
2
3
4
5//range里面传方法表示对每个k-v执行一般这个方法,返回true时就继续执行,直到返回false或者遍历结束,这里用来获取sync.map的k-v数
dict.m.Range(func(key, val interface{}) bool {
cnt++
return true
})
xlsx库
1.引包
1 | github.com/tealeg/xlsx |
接口
1.如果两个接口有相同的方法列表,那么他们就是等价的,可以相互赋值。如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A,B多余的方法会被忽略。
2.接口查询是否成功,要在运行期才能够确定
并发编程
1.在调用函数前加一个go关键字就是并发去执行该函数(使用的是协程)
- 当被调用的函数返回时,这个goroutine也自动结束了。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃
1
2
3
4
5
6
7
8func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(&wg)
}
wg.Wait() // 等待所有登记的goroutine都结束,不等待的话一旦main的goroutine退出,会把所有的子goroutine退出
}
2.chan类型的使用方法
- 声明,表示resultChan是容量为2的chan int类型
1
resultChan := make(chan int, 2)
- 使用
- 传值进去
1
2
3
4sum := 1
resultChan <- sum
sum++
resultChan <- sum - 取出来
1
2
3var sum1, sum2 int
sum1, sum2 = <- resultChan, <- resultChan
//因为管道是类似栈一样的先进后出,所以sum1=2,sum2=1
- 传值进去
3.停止一个goroutine
1 | quit = make(chan bool, 1) |
4.Go 当中同步锁有什么特点?作用是什么
- 当一个Goroutine获得了Mutex后,其他Goroutine就只能乖乖的等待,除非该Goroutine释放了该Mutex
- RWMutex在读锁占用的情况下,会阻止写,但不阻止读RWMutex。在写锁占用情况下,会阻止任何其他Goroutine(无论读和写)进来,整个锁相当于由该Goroutine独占(即读锁占用,只可以读,写锁占用,读和写都不可以)
- 同步锁的作用是保证资源在使用时的独有性,不会因为并发而导致数据错乱,保证系统的稳定性
5.Go 语言当中Channel(通道)有什么特点,需要注意什么?
- Go语言中,不要通过共享内存来通信,而要通过通信来实现内存共享。Go的CSP并发模型,中文可以叫做通信顺序进程,是通过goroutine和channel来实现的。channel收发遵循先进先出FIFO的原则
- 如果给一个nil 的channel 发送数据,会造成永远阻塞
- 如果从一个nil 的channel 中接收数据,也会造成永久阻塞
- 给一个已经关闭的channel 发送数据,会引起panic
- 从一个已经关闭的channel 接收数据,如果缓冲区中为空,则返回一个零值
6.Go 语言当中Channel 缓冲有什么特点?
- 无缓冲的channel是同步的
- 有缓冲的channel是非同步的
7.goroutine为什么会这么快?
- Go 语言标准库提供的所有系统调用操作(当然也包括所有同步IO 操作),都会出让CPU 给其他goroutine。这让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量
5.当只是用go关键字启动协程,然后不加等待语句主函数就结束的话,协程实际上并没有执行,因为当main()函数返回时,程序退出,且程序并不等待其他goroutine(非主goroutine)结束
6.使用共享内存+锁的方法并发
1 | func Count(lock *sync.Mutex){ |
7.<- chan类型在管道里没有数据时,会一直阻塞等待直到有数据。chan <- 写入后,会一直阻塞等待数据被读出去
8.channel的使用
- 声明
1
var ch chan int
- 声明及定义
1
ch := make(chan int)
- 写入,向channel写入数据通常会导致程序阻塞,直到有其他goroutine从这个channel中读取数据
1
ch <- value
- 读取,如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel中被写入数据为止
1
value := <-ch
9.select的使用,类似于switch
1 | select{ |
10.带缓冲的channel
- 定义及声明,在make时加上第二个参数即可
1
2c := make(chan int, 1024)
//即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。 - 读取全部缓冲用以下for range形式
1
2
3for i := range c {
fmt.Println("Received:", i)
}
11.超时机制利用到了select只要一个case为真就往下执行和time库
1 | timeout := make(chan bool, 1) |
12.单向channel
- 声明
1
2var ch2 chan<- float64// ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读取int数据 - 基于正常channel初始化成单向
1
2
3ch4 := make(chan int)
ch5 := <-chan int(ch4) // ch5就是一个单向的读取channel
ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel
13.关闭channel
- 直接close
1
close(ch)
- 用于检测channel是否关闭,同map一样
1
x, ok := <-ch
14.设置使得goroutine可以利用多个cpu核心
- 获取当前环境cpu数量
1
a := runtime.NumCPU()
- 设置使用a个cpu核心
1
runtime.GOMAXPROCS(a)
15.锁
- sync.Mutex。一个goroutine获得Mutex后,其他goroutine就直到阻塞等待它释放后才能执行
1
2
3c := &sync.Mutex{}
c.Lock()
defer c.Unlock() - sync.RWMutex。读锁被占用后只允许读不允许写,写锁被占用后读写都不允许
1
2
3b := &sync.RWMutex{}
b.Lock()
defer b.Unlock()
16.全局唯一操作
- sync.One类型
1
2
3var a sync.Once
a.Do(box()) //表示无论多少个goroutine运行到此行,box()全局中永远只被执行一次
17.ring buffer环形缓冲区实现
- channel 中使用了ring buffer(环形缓冲区) 来缓存写入的数据。ring buffer 有很多好处,而且非常适合用来实现FIFO 式的固定长度队列
- recvx 指向最早被读取的数据,sendx 指向再次写入时插入的位置
18.锁Mutex的几种形态
- mutexLocked —表示互斥锁的锁定状态;
- mutexWoken —表示从正常模式被从唤醒;
- mutexStarving —当前的互斥锁进入饥饿状态;
- waitersCount —当前互斥锁上等待的Goroutine 个数;
19.Mutex正常模式(性能较好)和饥饿模式
- 正常模式是不公平的,所有等待锁的goroutine按照FIFO(先进先出)顺序等待。但是最新请求的goroutine更容易抢占锁,因为它正在CPU上执行
- 饥饿模式是公平的,直接由unlock把锁交给等待队列中排在第一位的goroutine ,同时新进来的goroutine不会参与抢锁也不会进入自旋状态,会直接进入等待队列的尾部
- 饥饿模式的触发条件:当一个goroutine等待锁时间超过1毫秒时,或者当前队列只剩下一个goroutine的时候
20.允许自旋的条件
- Mutex不处于饥饿模式
- 当前自旋次数小于最大自旋次数(一般为4)
- cpu核心数大于1,且有空闲的核心
- 当前运行的goroutine挂载的核的待运行队列为空,即不存在有别的goroutine等着当前goroutine执行完成
21.读写锁RWMutex的原理
- 通过记录readerCount 读锁的数量来进行控制,当有一个写锁的时候,会将读锁数量设置为负数1<<30。目的是让新进入的读锁等待之前的写锁释放通知读锁。同样的当有写锁进行抢占时,也会等待之前的读锁都释放完毕,才会开始进行后续的操作。而等写锁释放完之后,会将值重新加上1<<30,并通知刚才新进入的读锁
- 属于单写多读锁,适用于读多写少的情景
- 解锁方式属于饥饿模式
- 一个goroutine可以解Unlock()另一个goroutine的Lock()
- 变量的0值是一个未锁定状态的互斥锁
22.map类型在大并发情况下是不安全的,在同时读和同时写的时候会退出程序,所以必须加锁
23.对某个结构体使用锁来实现并发安全的方法
- 定义的时候加上锁成员
1
2
3
4type xx struct{
sourceArray map[int]interface{}
sync.RWMutex //用*sync.RWMutex的话,会在初始化的时候创建锁,用sync.RWMutex只会在使用的时候创建锁,这两个方式在具体使用的时候没有区别
} - 使用的时候
1
2
3
4
5func (e *xx) update(){
e.Lock() //如果只读的话用e.RLock(),也可以写成e.RWMutex.Lock()效果一模一样
defer e.Unlock() //e.RLock()对应的用e.RUnlock()
//业务代码
} - ps:实例化结构体的时候*sync.RWMutex不需要赋值
反射
1.简单获取struct里的变量名和类型及值
1 | type Bird struct { |
编译相关
1.go run会直接执行编译、链接和运行而不产生中间文件
1 | go run main.go |
2.go build会执行编译产生一个main.exe文件
1 | go buil main.go |
项目相关
1.Go语言的项目有三个三个文件夹
- src文件夹:自己写的代码,第三方库的源代码
- bin文件夹:产生的二进制可执行文件
- pkg文件夹:生成的中间缓存文件
2.Go的目录
- GOROOT指的是go的安装目录
- GOPATH是添加到系统的环境变量
- 值可以是一个目录的路径,也可以是包含多个目录的路径,每个目录代表Go的一个工作区
3.新建一个Go项目步骤
- 新建Go项目
- 点击右上角“Add Configuration”
- 再弹出窗口选择+,然后选择“Go Build”
- 配置中Run kind选择Directory,其他的配置项作用如下(一般不用改)
- 名称:为本条配置信息的名称,可以自定义,也可以使用系统默认的值;
- Directory:用来设置 main 包所在的目录,不能为空;
- Output directory:用来设置编译后生成的可执行文件的存放目录,可以为空,为空时默认不生成可执行文件;
- Working directory:用来设置程序的运行目录,可以与“Directory”的设置相同,但是不能为空。
4.一个hello world程序
1 | package main |
5.安装第三方包,在命令行输入以下命令(注意每个新项目要重新安装一遍)
- 例如要import的包名是”github.com/taoshihan1991/imaptool/controller”
1
go get github.com/taoshihan1991/imaptool/controller
6.导入本地写好的包步骤
- 已知条件
- 主体项目名为Capt_Project(package main,主函数要是func main())
- 有一个包的目录为Capt_Project\captcha-client,其内有三个.go文件(package captcha_client)
- 按照约定,包名与导入路径的最后一个元素一致。例如,”Capt_Project\captcha-client” 包中的源码均以package captcha_client语句开始。
- 在main中使用包的导入语句是
1
2
3import (
"Captcha_Project/captcha-client"
)
7.把项目部署到linux上
- cmd控制台切换到项目分别执行以下命令
1
2
3
4set GOARCH=amd64
set GOOS=linux
go build main.go
//会在根目录得到一个test文件 - 把test文件放到linux里用运行即可
1
./test
- 依赖文件的上传方法
8.用以下形式导入的话,尽管包里是package router,但也要视作是package router2
1 | router2 "zhangshuai/router" |
9.用json文件给结构体赋值
- 引入json解析包,直接写就行不用下载
1
"encoding/json"
- 定义结构体
1
2
3
4type Student struct {
Name string `json:"name"`
Age int `json:"age"`
} - 读json文件并赋值
1
2
3
4
5
6
7//已知json文件里有{"name": "小明", "age": 123}的信息
dir, err := os.Getwd() //dir是项目绝对路径
s := new(Student)
file, err := os.Open("json文件相对项目路径")
confByte, err := ioutil.ReadAll(file)
json.Unmarshal(confByte, s)
fmt.Println(s) //会打印出&{小明 123} - 实战案例
- json文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16{
"app_port": "8082",
"app_secret": "FPlikJRgY82DpQ8tTbEngmJ0INREDjlw",
"mysql": {
"driver": "mysql",
"user": "root",
"pass_word": "123456",
"host": "127.0.0.1",
"port": "5726",
"db_name": "aoyi",
"charset": "utf8mb4",
"show_sql": true,
"parseTime": "true",
"loc": "Asia/Shanghai"
}
} - 结构体定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21type (
Config struct {
AppPort string `json:"app_port"`
AppSecret string `json:"app_secret"`
TemplateIdExperience string `json:"template_id_experience"`
TemplateIdTK string `json:"template_id_tk"`
MySql MySql
}
MySql struct {
Driver string `json:"driver"`
User string `json:"user"`
PassWord string `json:"pass_word"`
Host string `json:"host"`
Port string `json:"port"`
DbName string `json:"db_name"`
Charset string `json:"charset"`
ShowSql bool `json:"show_sql"`
ParseTime string `json:"parse_time"`
Loc string `json:"loc"`
}
) - 使用json赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18path := "json文件绝对路径"
config := new(Config)
file,err := os.Open(path)
if err != nil {
panic("打开配置文件错误")
}
confByte, err := ioutil.ReadAll(file)
if err != nil {
panic("读取配置文件错误")
}
err = json.Unmarshal(confByte,config)
if err != nil {
panic("解析配置文件错误")
}
//此时的实例config内部就已经全部是json的对应值了
- json文件
10.强制导入某个包即在前加个_即可,例如
1 | _ "github.com/go-sql-driver/mysql" |
11.go mod相关
- 要记得先在Goland里勾选Enable Go modules integration
- go.mod里存有一个项目的所有依赖,clone人家项目后要先在控制台用go mod tidy下载依赖
- xx是go.mod里的module名
1
2
3
4import (
"xx/internal/qyk_app_api/service/v1"
"xx/internal/qyk_app_api/store"
)
12.创建文件目录os.Mkdir、os.MkdirAll区别(os.ModePerm是固定写法,类似于一套参数)
- 如果dir1或dir2不存在,Mkdir会报错,MkdirAll会创建dir1/dir2/
- 如果dir已存在,Mkdir会报错,Mkdir会报错,MkdirAll会什么都不做
1
2os.Mkdir("dir1/dir2/dir3", os.ModePerm) //创建目录
os.MkdirAll("dir1/dir2/dir3", os.ModePerm) //创建多级目录
13.os.Creat(fileName)表示创建某文件,注意fileName应该是fileName = 绝对路径path + 文件名name 的形式
14.defer可以将一个方法延迟到包裹该方法的方法返回时执行,在实际应用中可以用来处理关闭文件句柄等收尾操作
- Go官方文档中对defer的执行时机做了阐述
- 包裹defer的函数返回时
- 包裹defer的函数执行到末尾时
- 所在的goroutine发生panic时
1
2
3
4f, _ := os.Create(fileName)
defer f.Close()
//这样使用的好处是可以在open后马上写close,而避免拖到最后才写close,有时候会忘记关闭文件
15.Go中swap的写法
1 | swap := reflect.Swapper(s) //s是任意类型的数组 |
16.序列化和反序列化
- 序列化是把结构体或map转成json格式
- 反序列化是把json数据赋给结构体或map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23type Student struct {
Name string `json:"name"` // 姓名
Course []string `json:"course"` // 课程
}
func main() {
//file是json文件的绝对路径
confByte, err := ioutil.ReadAll(file)
json.Unmarshal(confByte, s)
stu := Student{
"张三",
[]string{"语文", "数学", "音乐"},
}
data, err := json.Marshal(&stu) //将struct里的数据序列化为json格式
err := json.Unmarshal(confByte, &stu) //把confByte里的json数据反序列化放到struct
var map1 map[string]interface{} //使用一个空接口表示可以是任意类型
map1 = make(map[string]interface{}) //map1是一个map[string]interface{}变量
map1["name"] = "张三"
map1["hobby"] = [...]string{"看书", "旅游", "学习"}
data, err := json.Marshal(map1) //将map里的数据序列化为json格式
err := json.Unmarshal(confByte, &map1) //把confByte里的json数据反序列化放到map
}
17.tag的解释
1 | type param struct { |
- `form:”stu_no” json:”no” binding:”required”`这部分就是tag
- form:”stu_no”表示
- json:”no”表示
- binding:”required”表示
18.常用的数据结构
- []map[string]interface{}
- 定义
1
list := make([]map[string]interface{}, 0)
- 往里填数据
1
2
3
4list = append(list, map[string]interface{}{
"id": qyk_order.Id,
"stu_title": qyk_member.Title,
})
- 定义
- []map[string]interface{}定义和填数据
1
2
3
4
5
6data := map[string]interface{}{
"list": list,
"total": count,
"page": request.Page,
"static": static,
}
19.结构体的一些认识
- 结构体指针:=&xx
- 若xx是结构体类型名,则该新创建的一个结构体实例,并用该结构体指针指向它
- 若xx是结构体实例,则该结构体指针指向该结构体实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15type param struct {
BranchId int `form:"branch_id" json:"branch_id"`
Page int `form:"page" json:"page"`
Limit int `form:"limit" json:"limit"`
}
//request是结构体param的指针,下列语句表示新建一个param指针request并且给其中两个成员赋值
request := ¶m{
Limit: 10,
Page: 1,
}
ctx.Bind(request) //表示把*gin.Context中post传过来的json数据按param的tag赋值给request
//注意这里若Json中的Page和Limit有值,则会覆盖掉之前赋的Limit=10和Page=1
//即在Bind之前的赋值相当于一个指定默认值
- tag为json可以自动匹配json字段,tag为form可以自动匹配get的url参数字段,注意都需要以下bind操作
1
2request := ¶m{}
err := ctx.Bind(request)
20.go解析json数据成员的规则如下
1 | bool, for JSON booleans |
报错案例
1.“该版本的1%与您运行的windows版本不兼容”
解决办法:第一行package xx改成
1
package main
原理:run指定的go文件的package必须是main才可以运行,不是有main方法就可以的
- unrecognized import path
解决办法:go mod tidy更新一下就行