基础篇

变量类型

rune uint8

    // 遍历字符串
    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()
    }

结果是

    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码的范围就做不到了,像这种中文的就打印不出来

修改字符串

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

数组赋值

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{} 这个准确来说是

s2:=[]int{

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

s3:=make([]int,0)

结构体

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}
*/

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

//这个是个常见的错误例子,因为用的是&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后直接跟自定义的函数名,而方法是要先声明接收者变量和接收者类型

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

举个栗子

//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()
}

指针类型的接收者

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)
}

接口

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对象的一种方式,键/值对组合中的键名写在前面并用双引号”“包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

//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 语言的重要基础。让我用一个简单的例子来说明这些概念之间的关系。

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

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

type Rectangle struct {
    Width  float64
    Height float64
}

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

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

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

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

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

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.关闭链接

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处理链接
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编程

服务端

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("这是服务器发来的消息"))
}

客户端

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:端口/路径来访问,等着再学学再说吧)

并发

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是否关闭

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 随机生成数字进行计算

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

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字段发送当时的时间。
//上面这两个函数加起来就是计时器

正式程序

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

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则说明通道内无数据或者通道关闭

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

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

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暂时好像还用不到,先过

并发安全和锁

//上面的代码中我们开启了两个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,即互斥锁的英文

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

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看完再来过一遍吧

// 我们填写一个示例来比较下互斥锁和原子操作的性能。

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包的函数/类型实现同步更好。

爬虫

爬虫步骤

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

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上下文

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是一个接口,签名如下

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

函数签名如下

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

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

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()出意外没有调用时的保底手段 签名如下

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

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

WithTimeOut

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

WithValue

函数签名如下

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

反射

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

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

反射获取interface值信息

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类型信息

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
*/

反射修改值信息

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
*/

查看类型、字段和方法

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

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)
	}
}

Category: go | Tags: go | Created: 2024-11-30 18:19:09