go查漏补缺

Last updated on 4 months ago

基础篇

变量类型

rune uint8

1
2
3
4
5
6
7
8
9
10
11
12
// 遍历字符串
func traversalString() {
s := "pprof.cn博客"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}

结果是

1
2
112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174(®) 162(¢)
112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338(博) 23458(客)

可以uint8是单独取出字符串中每个元素的ascii码进行处理,超出ascii码的范围就做不到了,像这种中文的就打印不出来

修改字符串

要修改一个字符串,首先需要把字符串转换成数组类型,然后再进行修改

数组赋值

1
2
3
4
5
6
7
8
9
10
11
12
var c [5]int{2:40,4:50}
/*
c[0] = 0
c[1] = 0
c[2] = 100
c[3] = 0
c[4] = 200
*/
for _,i := range c{
fmt.println(i)
}
//for range 会返回索引和值,要用大括号

%s打印字符串
%p是打印地址
%v是检测变量类型自动打印
%#v是检测变量类型并详细的自动打印

切片

var s1 []int
s2:=[]int{}
这个准确来说是

1
2
3
4
s2:=[]int{

}
//{}内可写内容,比如s2:=[]int{1,2,3},不写的话就是[]int{},代表建立了一个空切片

s3:=make([]int,0)

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main 
import(
"fmt"
)
type student struct{
name string
age int
}
//创建对象方式1
func main(){
var p1 student
p1.name="yblue"
p1.age=18
fmt.Printf("%#v\n%v\n", p1, p1)
/*
main.student{name:"yblue", age:18}
{yblue 18}
*/
//创建对象方式2
p2:=student{}
/*这里准确的写法是
p2:=student{

}
也可以这样
p2:=student{
name: "usr0",
}
后面必须要跟逗号, 即使是最后一个键值对也要加
*/
p2.name="usr0"
fmt.Printf("%#v\n%v\n", p2, p2)
/*
main.student{name:"usr0", age:0}
{usr0 0}
*/
//创建对象方式3
p3:=&student{
name: "root",
}
fmt.Printf("%#v\n%v\n", p3, p3)
}
/*
&main.student{name:"root", age:0}
&{root 0}
*/

还有可能有别的创建结构体的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//这个是个常见的错误例子,因为用的是&stu是把每一个m[stu.name]指向了&stu这个地址,而没经过一次循环,这个地址的内容就会变成新的,到最后3个m[stu.name]指向的都会是最后一个的值
type student struct {
name string
age int
}

func main() {
m := make(map[string]*student)
stus := []student{ //这里是创建了一个student类型的切片 切片创建是 var s1 []int 这里的student{...}就跟int等价
{name: "pprof.cn", age: 18},
{name: "测试", age: 23},
{name: "博客", age: 28},
}

for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}

方法和接收

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。
跟c++的成员函数差不多,只有对应的接收者(go是结构体,c++是类)才能使用方法/成员函数

自定义函数跟方法的组成区别

自定义函数是func newPerson(name, city string, age int8) *person {} func后直接跟自定义的函数名,而方法是要先声明接收者变量和接收者类型

1
2
3
4
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
//官方建议接收者变量命名为接收者类型的第一个小写字母,比如接收者类型是Study这个结构体,那接收者变量最好命名为s

举个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//Person 结构体
type Person struct {
name string
age int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}

//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
p1 := NewPerson("测试", 25)
/*
p1:=person{
测试,
25,
}
*/
p1.Dream()
}

指针类型的接收者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
func (p *Person) SetAge(newage int){ //如果这里不是p *Person,那么再次打印时值还是18
p.age=newage
}
type Person struct{
age int
name string
}
func main(){
p1:=Person{
18,
"小明",
}
fmt.Println(p1.age)
p1.SetAge(20)
fmt.Println(p1.age)
}

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type Person interface {
GetName()
}
type Student struct {
Name string
Age int
}

func (stu Student) GetName() {
fmt.Println(stu.Name)
}
func main() {
s := Student{
Name: "yblue",
Age: 18,
}
var s1 Person = s
s1.GetName()
}

结构体与JSON序列化

JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号””包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//Student 学生
type Student struct {
ID int
Gender string
Name string
}

//Class 班级
type Class struct {
Title string
Students []*Student
}

func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}

结构体,字段,方法,类型大统一

理解结构体、字段、方法和类型之间的关系是理解 Go 语言的重要基础。让我用一个简单的例子来说明这些概念之间的关系。

假设我们想创建一个程序来表示矩形,并能够计算其面积。我们可以使用结构体、字段、方法和类型来完成这个任务。

首先,我们定义一个矩形的结构体:

1
2
3
4
type Rectangle struct {
Width float64
Height float64
}

在上面的代码中,我们创建了一个名为 Rectangle 的结构体,它有两个字段:Width 和 Height,分别表示矩形的宽度和高度。

接下来,我们可以为这个结构体定义一个方法来计算矩形的面积:

1
2
3
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

在上面的代码中,我们为 Rectangle 结构体定义了一个名为 Area 的方法。这个方法接收一个 Rectangle 类型的接收者(receiver),并返回一个 float64 类型的面积值。方法可以访问接收者结构体的字段,这里我们用 r.Width 和 r.Height 计算了面积。

现在,我们可以创建一个 Rectangle 类型的对象,并使用它的 Area 方法来计算面积:

1
2
3
4
5
func main() {
rect := Rectangle{Width: 5.0, Height: 3.0}
area := rect.Area()
fmt.Printf("矩形的面积是: %f\n", area)
}

在上面的 main 函数中,我们创建了一个 Rectangle 类型的对象 rect,并调用了它的 Area 方法来计算面积。最终,我们打印出了矩形的面积。

这个例子涵盖了以下概念:

结构体 (Rectangle):用于组织相关字段的数据结构。
字段 (Width 和 Height):结构体中的属性或数据成员。
方法 (Area()):与特定类型关联的函数,用于执行特定的操作。
类型 (Rectangle):自定义的数据类型,具有自己的字段和方法。
希望这个例子有助于你理解结构体、字段、方法和类型之间的关系。如果你有更多问题或需要进一步解释,请随时提问。

网络编程

socket

看不懂搞不明白,过

tcp网络编程

创立客户端(client)

一个TCP客户端进行TCP通信的流程如下:
1.建立与服务端的链接
2.进行数据收发
3.关闭链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"bufio"
"fmt"
"log"
"net"
"os"
"strings"
)

func main() {
conn, err := net.Dial("tcp", ":12345")
if err != nil {
log.Fatalln(err)
}
defer conn.Close()
input := bufio.NewReader(os.Stdin)
for {
//检测用户输入并传递给服务端(send)
ip, _ := input.ReadString('\n')
ipinfo := strings.Trim(ip, "\n\r")
if strings.ToUpper(ipinfo) == "Q" {
return
}
_, err := conn.Write([]byte(ipinfo)) //将用户的输入先强制转换成字节类型的切片,再发送给服务器
if err != nil {
log.Fatalln(err)
}
//接收服务端的消息(recv)
buf := [512]byte{} //创建一个字节类型的数组([]里面写length就是数组,没写就是切片),用来存放服务端传递的数据(本例中服务端并未给客户端发送数据)
n, err := conn.Read(buf[:])
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(buf[:n]))
}
}

建立服务端(server)

TCP服务端程序的处理流程:

1.监听端口
2.接收客户端请求建立链接
3.创建goroutine处理链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"bufio"
"fmt"
"log"
"net"
)

func process(conn net.Conn) {
defer conn.Close()
//创建读取对象
reader := bufio.NewReader(conn)
for {
//建立存储客户端发送请求的数组
rd := [512]byte{}
//向数组写入内容
n, err := reader.Read(rd[:])
if err != nil {
log.Fatalln(err)
}
//打印读取的内容
fmt.Println(string(rd[:n]))
//向客户端发送内容
recv := fmt.Sprintf("你已成功发送,数据为:%s", string(rd[:n]))
conn.Write([]byte(recv))
}
}
func main() {
listen, err := net.Listen("tcp", ":12345")
if err != nil {
log.Fatalln(err)
}
for {
//建立连接
conn, err := listen.Accept()
if err != nil {
log.Fatalln(err)
}
go process(conn)
}
}

UDP网络编程

就是把前面的tcp换成udp,但教程还给了udp专门的socket的函数,但是感觉没什么大用,碰到再学吧

tcp黏包

就是数据发送过多,让数据包重合了

http编程

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"net/http"
)

func main() {
//处理路由,当路由为/index时,触发myHandler的函数进行处理
http.HandleFunc("/index", myHandler)
//这里nil的意思是,除了127.0.0.1:1234/index以外的url访问,都会使用默认处理规则
http.ListenAndServe("127.0.0.1:1234", nil)
}
func myHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
//这里的w跟r是在有外界访问127.0.0.1:1234/index时默认传递的参数
/*
w http.ResponseWriter 是一个用于向客户端发送 HTTP 响应的接口。通过这个接口,您可以设置响应头、写入响应体等。
在 myHandler 函数中,w 用于构建响应并发送给客户端。
r *http.Request 是一个表示 HTTP 请求的结构体。它包含了关于客户端请求的信息,如请求方法、URL、请求头、请求体等。
在 myHandler 函数中,r 用于访问客户端的请求信息,以便根据请求执行相应的操作。
*/
//简单来说w是服务端对客户端进行的操作,而r是客户端发来的请求对象,以下是对r的操作
fmt.Println(r.RemoteAddr, ":连接成功")
fmt.Println("method:", r.Method)
fmt.Println("url:", r.URL.Path)
fmt.Println("header:", r.Header)
fmt.Println("body:", r.Body)
//这里的w是服务端对客户端的操作
w.Write([]byte("这是服务器发来的消息"))
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"io"
"log"
"net/http"
)

func main() {
resp, err := http.Get("http://127.0.0.1:1234/index")
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status)
fmt.Println(resp.Header)
buf := make([]byte, 1024)
for {
n, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
log.Fatalln(err)
} else {
fmt.Println("读取完成")
fmt.Println(string(buf[:n]))

}
}
}

websocket编程(聊天室)

https://github.com/taosu0216/go_stu/tree/main/Internet_coding/web_socket
基本完成,代码都能看懂但是纯自己写应该是写不出来,前端代码没看,项目不完整(不能ip:端口/路径来访问,等着再学学再说吧)

并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func hello(i int) {
defer wg.Done()
fmt.Println("Goroutine ", i, " 号开始执行")
}
func main() {
for i := 1; i <= 10; i++ {
wg.Add(1)
go hello(i)
go hello(i + 10)
}
wg.Wait()
}

channel

通道可以关闭,但关闭通道不是必须的
ch:=make(chan []int,65535)
此时的ch就是一个用于接收数值数组的,后面的数字代表通道最大容量,可以不加
close(ch)
这是关闭通道的操作,关闭通道可以再从channel中读取,但是不能再存入了

channel分为有缓冲和无缓冲
无缓冲就是在创建channel时不加数字,此时相当于一个单纯的消息通道作用,有传入就必须有接收,否则会发生恐慌
无缓冲通道又叫同步通道
有缓冲就是有数字,此时channel类似快递站,可以暂存消息,有信息传入channel后,可以不用立刻有接收方

判断channel是否关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
)

func main() {
ch1 := make(chan int, 20)
ch2 := make(chan int, 20)
go func() {
for i := 0; i <= 10; i++ {
ch1 <- i
}
close(ch1)
}()
go func() {
for {
//第一种判断channel是否关闭的方法,如果channel关闭则ok为false
i, ok := <-ch1
if !ok {
break
}
ch2 <- i * i
}
defer close(ch2)
}()
//第二种判断channel是否关闭的方法,如果关闭则会自动退出range循环
for i := range ch2 {
fmt.Println(i)
}
}

worker pool(Goroutine池)


计算一个数字的各个位数之和,例如数字123,结果为1+2+3=6
随机生成数字进行计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import (
"fmt"
"math/rand"
)

type Result struct {
job *Job
sum int
}
type Job struct {
Id int
Random_number int
}

func main() {
//先建立需要的channel,分别是将job传给工人的channel和结果的channel
send_channel := make(chan *Job, 128)
result_channel := make(chan *Result, 128)
//将channel传入工人池,第一个64是job的数量
worker_pool(64, send_channel, result_channel)
//接收结果并打印
go func(result_c chan *Result) {
for result := range result_c {
fmt.Println("第", result.job.Id+1, "个job,它的随机值是:", result.job.Random_number, "是它的结果是:", result.sum)
}
}(result_channel)
//defer关闭channel
defer close(send_channel)
defer close(result_channel)
//建立将job传入channel的函数(正常来说这一步应该是放在前面,但是文档这里是无限循环,所以放在最后,我这里改成有限循环了,注意这里循环值也就是循环次数,i的最大值要大一些,否则程序可能没来得及打印就推出了)
for i := 0; i < 1000; i++ {
rand_num := rand.Int()
job := &Job{
Id: i,
Random_number: rand_num,
}
send_channel <- job
}
// time.Sleep(2 * time.Second)
}

// 创建工人池
func worker_pool(num int, sc chan *Job, rc chan *Result) {
for i := 0; i < num; i++ {
go func(sc chan *Job, rc chan *Result) {
for job := range sc {
//获取随机数并进行处理
r_num := job.Random_number
//sum就是求和的结果,要把sum传进Result结构体,并将Result传入rc通道
sum := 0
for r_num != 0 {
tmp := r_num % 10
sum += tmp
r_num = r_num / 10
}
re := &Result{
job: job,
sum: sum,
}
rc <- re
}
}(sc, rc)
}
}

定时器

timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Timer struct{
C <- chan Time
}
type Time struct {
// 内含隐藏或非导出字段
}
/*
Time代表一个纳秒精度的时间点。

程序中应使用Time类型值来保存和传递时间,而不能用指针。就是说,表示时间的变量和字段,应为time.Time类型,而不是*time.Time.类型。
一个Time类型值可以被多个go程同时使用。时间点可以使用Before、After和Equal方法进行比较。
Sub方法让两个时间点相减,生成一个Duration类型值(代表时间段)。
dd方法给一个时间点加上一个时间段,生成一个新的Time类型时间点。

Time零值代表时间点January 1, year 1, 00:00:00.000000000 UTC。因为本时间点一般不会出现在使用中,IsZero方法提供了检验时间是否显式初始化的一个简单途径。

每一个时间都具有一个地点信息(及对应地点的时区信息),当计算时间的表示格式时,如Format、Hour和Year等方法,都会考虑该信息。Local、UTC和In方法返回一个指定时区(但指向同一时间点)的Time。修改地点/时区信息只是会改变其表示;不会修改被表示的时间点,因此也不会影响其计算。
*/
func Now() Time
//Now返回当前本地时间。

func NewTimer(d Duration) *Timer
//NewTimer创建一个Timer,它会在最少过去时间段d后到期,向其自身的C字段发送当时的时间。
//上面这两个函数加起来就是计时器

正式程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println(time.Now())
//写入数据是 channel <- data
//读取数据时 data <- channel
t := <- time.NewTimer(time.Second).C
fmt.Printf("%v", t)

timer2 := time.NewTimer(time.Second)
for {
//这是 Go 语言的一个特性,我们可以直接从通道接收数据,而不需要指定接收变量。这种方式我们称为"丢弃接收"。
<-timer2.C
fmt.Println("时间到")
}

fmt.Println(time.Now())
t1 := time.NewTimer(2 * time.Second)
<-t1.C
fmt.Println("过去了2秒")

t := time.NewTimer(time.Second)
go func() {
<-t.C
fmt.Println("收到时间")
}()
//只有在这里睡2秒钟(及以上)才可以打印"收到时间",否则都是已关闭
time.Sleep(2 * time.Second)
for t.Stop() {
fmt.Println("已关闭")
}
// 5.重置定时器
timer5 := time.NewTimer(3 * time.Second)
timer5.Reset(1 * time.Second)
fmt.Println(time.Now())
fmt.Println(<-timer5.C)

}

ticker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"time"
)

func main() {
// 1.获取ticker对象
ticker := time.NewTicker(1 * time.Second)
i := 0
// 子协程
go func() {
for {
//<-ticker.C
i++
//这个语法也是合理的
fmt.Println(<-ticker.C)
if i == 5 {
//停止
ticker.Stop()
}
}
}()
for {

}
}

timer和ticker的区别
Ticker在Go语言中是专门用来实现定期任务的一个结构体。它的英文意思是“计时器”。
具体来说:
Ticker代表一个计时器,可以定期生成时间脉冲。
它通过时间通道(channel)定期地发送当前时间。
不同于Timer是一次性的,Ticker是自动重复工作的。

使用Ticker的主要场景包括:
需要每隔一定时间就执行一次的重复任务,比如每隔1秒打印一次日志。
需要基于时间来驱动和同步其他goroutine工作,例如心跳检测。
需要周期性执行某些操作而不是手动定时,如每10分钟保存一次数据。

Ticker的工作流程:
使用time.NewTicker创建一个Ticker对象
它会打开一个时间通道
从该通道中周期性读取时间信号
根据时间信号驱动后续任务执行
可以调用Stop停止Ticker工作

select

_,ok := <- channel
占位符代表的是从channel中拿取的数据,ok(bool)是判断是否有数据,如果ok为false则说明通道内无数据或者通道关闭

1
2
3
4
5
6
7
8
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}

如果多个通道同时就绪,则只选择一个随机执行,剩下的数据都被丢弃,所以写的时候要注意好逻辑,否则可能会丢失数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
)

func main() {
// 创建2个管道
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
go func() {
//time.Sleep(2 * time.Second)
int_chan <- 1
}()
go func() {
string_chan <- "hello"
}()
select {
case value := <-int_chan:
fmt.Println("int:", value)
case value := <-string_chan:
fmt.Println("string:", value)
}
fmt.Println("main结束")
}

sync


讲的很清晰明了了,就是var wg sync.WaitGroup的时候要定义成全局变量
sync.Once和sync.Map暂时好像还用不到,先过

并发安全和锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。
var x int64
var wg sync.WaitGroup

func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}

Mutex, mutual exclusion,即互斥锁的英文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

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

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
for i := 0; i < 200; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
wg.Done()
}
func main() {
fmt.Println(time.Now())
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x, time.Now())
}

读写锁

互斥锁是所有操作都被禁止,但是大部分应用场景是读多写少,互斥锁使用时不能读不能写,会堵塞进程降低性能,这个时候就可以使用读写锁.
RWmutex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

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

var (
x int64
wg sync.WaitGroup
// lock sync.Mutex
rwlock sync.RWMutex
)
//模拟写操作,睡10ms
//一个操作里有读写锁的关上与打开和计数器减一,三个必要操作
func write() {
rwlock.Lock()
x = x + 1
rwlock.Unlock()
time.Sleep(10 * time.Millisecond)
wg.Done()
}
//模拟读操作,睡1ms
func read() {
rwlock.RLock()
time.Sleep(time.Millisecond)
rwlock.RUnlock()
wg.Done()
}
func main() {
start := time.Now()
//模拟实际情况,读远大于写
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
for j := 0; j < 10; j++ {
wg.Add(1)
go write()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}

原子操作

看的不是很懂,把gmp看完再来过一遍吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 我们填写一个示例来比较下互斥锁和原子操作的性能。

var x int64
var l sync.Mutex
var wg sync.WaitGroup

// 普通版加函数
func add() {
// x = x + 1
x++ // 等价于上面的操作
wg.Done()
}

// 互斥锁版加函数
func mutexAdd() {
l.Lock()
x++
l.Unlock()
wg.Done()
}

// 原子操作版加函数
func atomicAdd() {
atomic.AddInt64(&x, 1)
wg.Done()
}

func main() {
start := time.Now()
for i := 0; i < 10000; i++ {
wg.Add(1)
// go add() // 普通版add函数 不是并发安全的
// go mutexAdd() // 加锁版add函数 是并发安全的,但是加锁性能开销大
go atomicAdd() // 原子操作版add函数 是并发安全,性能优于加锁版
}
wg.Wait()
end := time.Now()
fmt.Println(x)
fmt.Println(end.Sub(start))
}
// atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。
// 除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

爬虫

爬虫步骤

明确目标(确定在哪个网站搜索)
爬(爬下内容)
取(筛选想要的)
处理数据(按照你的想法去处理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
"fmt"
"io"
"net/http"
"os"
"regexp"
"time"
)

var reQQemail = `(\d+)@qq.com`

// 爬取邮箱跟qq的函数
func Getemail(file *os.File) {
//计算爬取时间
start := time.Now()
//对网站进行访问及爬取
resp, err := http.Get("https://www.xyfinance.org/hot/516352")
HandleErr(err, "http.Get url")
defer resp.Body.Close()
//读取网站的全部响应内容
pagebody, err := io.ReadAll(resp.Body)
HandleErr(err, "io.ReadAll")
//pagebody本来是字节切片,这里转换成字符串格式方便操作
pageStr := string(pagebody)
/*
这行代码首先使用 regexp.MustCompile 函数来将正则表达式模式 reQQemail 编译为一个可重复使用的正则表达式对象 re。
这将创建一个用于匹配 reQQemail 模式的正则表达式。
*/
re := regexp.MustCompile(reQQemail)
/*
使用之前创建的正则表达式 re 在字符串 pageStr 中查找所有匹配 reQQemail 模式的子字符串,并将结果存储在 results 变量中。
-1 表示查找所有匹配项。如果是正整数则表示匹配次数,而-1则是有多少匹配多少
*/
results := re.FindAllStringSubmatch(pageStr, -1)
//这里的results是一个二维切片,result是一个一维切片,reslut[0]相当于results[0][0]
for index, result := range results {
email := result[0]
qq := result[1]
fmt.Println("email:", email)
fmt.Println("QQ:", qq)
fmt.Fprintf(file, "%d. \nemail:%s\n", index+1, email)
fmt.Fprintf(file, "QQ:%s\n", qq)
}
end := time.Now()
fmt.Println("线程用时:", end.Sub(start))

}
func HandleErr(err error, why string) {
if err != nil {
fmt.Println(why, err)
}
}
func main() {
//在当前目录下建立txt文件
file, err := os.Create("tmp/qq_email.txt")
HandleErr(err, "os.Create")
defer file.Close()
//爬取邮箱及qq
Getemail(file)
}

并发爬虫下载图片(速度非常慢)
https://github.com/taosu0216/go_stu/tree/main/Spider

context上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

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

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("worker")
time.Sleep(1 * time.Second)
select {
//这里的ctx.Done()不是一个函数,而是一个channel,在cancel()执行时,会关闭这个channel。
//当一个channel被关闭后,再从这个channel接收数据,会获得该channel类型的零值。
//也就是说当cancel()执行时,会关闭这个channel,然后case接收到一个0值,就会执行break跳出LOOP
case <-ctx.Done():
//这里的break LOOP是退出被LOOP标签包裹的所有循环(这里是for+select)
//如果不是break LOOP的话会导致继续进入for循环
break LOOP
//这里的只有case的话可能会被堵塞,所以加一个空default
default:
}
}
wg.Done()
}

func main() {
//接收一个上下文,并返回一个新的上下文和一个取消函数
//context.Background()用于创建一个空的根上下文(root context)。
/*
根上下文是一个空的上下文,它没有任何与之相关联的值或取消机制。
它通常作为其他上下文的父上下文(parent context)使用,
可以通过调用context.WithCancel、context.WithDeadline、context.WithTimeout等函数
创建一个带有取消功能的上下文。

这是context.Background()函数的函数签名:
func Background() Context
*/
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go worker(ctx)
time.Sleep(4 * time.Second)
cancel()
wg.Wait()
fmt.Println("over")
}

context.Context是一个接口,签名如下

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}

Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline)

Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel

Err方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值
如果当前Context被取消就会返回Canceled错误
如果当前Context超时就会返回DeadlineExceeded错误

Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据

Background()和TODO()

Go内置两个函数:Background()和TODO(),这两个函数分别返回一个实现了Context接口的background和todo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。

Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

background和todo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

With函数

此外,context包中还定义了四个With系列函数。

WithCancel

函数签名如下

1
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。
取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func gen(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
//这里的return类似前面的break LOOP,就是跳出整个循环
return // return结束该goroutine,防止泄露
case dst <- n:
n++
}
}
}()
return dst
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 当我们取完需要的整数后调用cancel

for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}

WithDeadline

通常是用cancel()就可以做到,但是有些时候可能会因为各种原因没有调用cancel(),这里的WithDeadline就是在cancel()出意外没有调用时的保底手段
签名如下

1
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

设置dead时间,并返回一个新的上下文对象,这个新的上下文对象是对原来的上下文对象的复制,但是多了生效时间,后面再给函数传参时就可以传递这个新的对象而不是原来的对象
除了在超时之外自动取消,也可以用返回的cancelFunc手动取消

WithTimeOut

跟上面差不多,但timeout是隔多长时间后自动停止,设置的是一个最大时间,而前面的deadline则是具体的时间点,是绝对时间

WithValue

函数签名如下

1
func WithValue(parent Context, key, val interface{}) Context

反射

这篇讲的挺细的
https://juejin.cn/post/6844903559335526407?searchId=202310120916499405104F20617909405F

reflect包封装了反射相关的方法
获取类型信息:reflect.TypeOf,是静态的
获取值信息:reflect.ValueOf,是动态的

反射获取interface值信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"reflect"
)

func main() {
x := 123.456
reflect_value(x)
}
func reflect_value(x interface{}) {
v := reflect.ValueOf(x)
fmt.Println(v)
fmt.Printf("%T\n", v)
k := v.Kind()
fmt.Println(k)
fmt.Printf("%T\n", k)
switch k {
//这里的reflect.Int中的Int可以换成任何变量类型,String,Struct等等都可以
case reflect.Int:
fmt.Println("是", v.Int())
default:
//这里看不懂用v.Float()的作用,感觉跟直接打印v是一样的
fmt.Println("不是",v.Float())
}
}

/*
123.456
reflect.Value
float64
reflect.Kind
不是
*/

反射获取interface类型信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"reflect"
)

func main() {
x := 23.45
re(x)
}
func re(x interface{}) {
value := reflect.TypeOf(x)
fmt.Println("x的类型是:", value)
//这里说的是.Kind()可以获取更具体的信息
k := value.Kind()
fmt.Println(k)
//注意这里是switch,select是channel用的
switch k {
case reflect.Float64:
fmt.Println("x的类型是", k)
default:
fmt.Println("不是float64")
}
}
/*
x的类型是: float64
float64
x的类型是 float64
*/

反射修改值信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"fmt"
"reflect"
)

func main() {
x := 3.14
fmt.Println("一开始的x:", x)
re(&x)
fmt.Println("修改后的x:", x)
}
func re(x interface{}) {
v := reflect.ValueOf(x)
fmt.Printf("类型是:%T,值是:%v\n", v, v)
k := v.Kind()
switch k {
/*
v.Elem().SetFloat():
用于修改反射值中的底层值,通常用于修改指向可设置类型的指针所指向的值。
如果 v 是一个指针,v.Elem() 会返回指针所指向的值,并且只有这个值是可设置的(例如,可设置的变量),才能够使用 v.Elem().SetFloat() 来修改它。

v.SetFloat():
用于修改反射值中的底层值,但只能用于直接包含可设置类型的反射值。
如果 v 本身是一个可设置的反射值,且其类型兼容 float64,则可以使用 v.SetFloat() 直接修改该值。
*/

case reflect.Float64:
v.SetFloat(567.89)
fmt.Println("case 1 is ", v)
case reflect.Ptr:
//Elem()是获取地址指向的值
v.Elem().SetFloat(12.3)
fmt.Println("case 2 is ", v)
//地址
fmt.Println(v.Pointer())
}
}
/*
一开始的x: 3.14
类型是:reflect.Value,值是:0xc00001a0b8
case 2 is 0xc00001a0b8
824633827512
修改后的x: 12.3
*/

查看类型、字段和方法

很乱,看的头晕,以后再学

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

func (u User) Hello() {
fmt.Println("hello")
}

// var u1 = User{
// Id: 2,
// Name: "小f",
// Age: 19,
// }

func main() {
u := User{
Id: 1,
Name: "小明",
Age: 18,
}
Poni(u)
}
func Poni(u interface{}) {
//获取类型信息
ty := reflect.TypeOf(u)
//这里的打印值为类型为: main.User ,这里的main.User是指在main包下定义的结构体
fmt.Println("类型为: ", ty)
fmt.Println("字符串类型: ", ty.Name())
//获取值信息
value := reflect.ValueOf(u)
fmt.Println("reflect.ValueOf(u) = ", value)
// 获取属性
// 获取结构体字段个数:t.NumField()
for i := 0; i < ty.NumField(); i++ {
// 获取每个结构体字段名:ty.Field(i)
// 获取每个结构体字段类型:ty.Field(i).Type()
v := ty.Field(i)
fmt.Println("value.Field(i) = ", v)
fmt.Printf("%s : %v\n", v.Name, v.Type)
val := value.Field(i).Interface()
fmt.Println("value.Field(i).Interface() = ", val)
}
fmt.Println("----------------------方法---------------------")
for i := 0; i < ty.NumMethod(); i++ {
m := ty.Method(i)
fmt.Println("m.Name: ", m.Name, " m.Type: ", m.Type)
}
}

go查漏补缺
https://blog.yblue.top/2023/09/26/go查漏补缺/
Posted on
September 26, 2023
Licensed under