文章目录
- 前言
- 关于Go
- 学习流程
 
- 基础语法
- 注释
- 变量
- 常量
- 数据类型
- 运算符
- fmt库
 
- 流程控制
- if、switch、select
- for、break、continue
- 遍历String
 
- 函数
- 值传递和引用传递
- defer
- init
- 匿名、回调、闭包函数
 
- 数组和切片
- Map
- 结构体
- 自定义数据类型
- 接口
- 协程和channel
- 线程锁
- 异常处理
- 泛型
- 文件读取
- 文件写入
- 反射
- TCP网络编程
- Http
- websocket
- 爬虫
- 正则表达式
- goquery
- colly
- 豆瓣250
- 爬B站评论
 
前言
简单的入门一下Go、会对基础语法、网络编程、Gin开发进行简单的了解,关键是Gin开发。
关于Go
Go语言(也称为Golang)是Google开发的一种开源编程语言。它被设计用于构建高效、可靠和可扩展的软件系统。下面是Go语言的一些主要用途:
- 服务器端开发:Go语言提供了强大的标准库和并发模型,使其成为构建高性能网络服务器的理想选择。许多大型互联网公司正在使用Go语言来开发后端服务,以处理高负载和并发请求。
- 网络编程:Go语言提供了丰富的网络编程库,可用于开发各种网络应用程序,包括Web服务、API服务器、网络代理等。
- 分布式系统:Go语言的并发模型和原生支持的并发原语(goroutine和channel)使其非常适合构建分布式系统,例如数据处理管道、消息队列等。
- 命令行工具:Go语言的编译速度快,生成的可执行文件体积小,使其成为开发命令行工具的良好选择。许多开发者使用Go语言来构建工具、脚本和自动化任务。
- 嵌入式系统:Go语言可以用于编写嵌入式系统的控制逻辑和驱动程序。它提供了对底层硬件的访问和控制能力,并具有较小的内存消耗。
需要注意的是,Go语言具有简洁而直观的语法,易于学习和使用。它的性能非常好,可以充分利用多核处理器和并发编程来提高应用程序的性能和吞吐量。
学习流程
基础语法->Web开发->常见中间件->云平台
基础语法
注释
增强语言的可读性
-  单行注释 
-  多行注释 package main import "fmt" // 单行注释 /* 多行注释 多行注释 */ func main() { fmt.Println("hello world") }
变量
- var定义变量,- var 变量名 变量类型。
- 简短变量声明,使用:=运算符可以在函数内部声明并初始化变量。
- 匿名变量,使用 _占位符可以声明一个匿名变量,忽略不需要的值,任何赋值給这个标识符的值都将被抛弃,并且不会导致变量的冲突
- 定义多个变量,可以使用()包裹,表示定义多个变量
- 注意点:变量名的首个字符不能为数字,全局变量可被局部变量再定义(就近原则),定义的变量一定要使用。
变量声明后的默认值:
- 整数型浮点数变量默认值是0和0.0
- 字符串变量默认值是空字符串
- 布尔型变量默认是false
- 切片,函数,指针变量默认是nil
Printf输出时声明的格式
- %v:默认格式化输出,会根据变量的类型自动选择合适的格式。
- %s:输出字符串。
- %d、- %b、- %o、- %x:输出整数,分别表示十进制、二进制、八进制和十六进制。
- %t:输出布尔值,结果为 true 或 false。
- %f、- %e、- %g:输出浮点数,分别表示十进制表示法、科学计数法和通用格式。
- %p:输出指针地址。
- %c:输出字符。
- %q:输出带引号的字符串。
- %%:输出一个百分号。
fmt.Printf("内存地址:%p,变量类型:%T",name,&name) //打印内存地址,变量类型等
例子:
package main
import "fmt"
var name = "Lau" //全局变量(隐式定义)
func fun() (int, int) {
	return 100, 200
}
func main() {
	var name string = "aiwin" //显示定义
	var a, b int //同时定义a,b两个变量
	age := 18
	fmt.Printf("姓名:%s,内存地址为:%p,类型为:%T,年龄:%d,年龄十六进制数为:%x\n", name, &name, name, age, age) //就近原则
	a, _ = fun()
	_, b = fun()
	fmt.Println("_可代替被舍弃的值,并且可被重复定义:", a, b)
	a, b = b, a
	fmt.Println("类似于Python,可直接进行值交换:", a, b)
}
常量
- 使用const来定义常量,不可改变
- iota开始是0,默认会不断的自增进行计数,相当于是一个常量的- 计数器,直至新的一组常量计数器出现才会恢复,可理解过- const语句块的- 索引
package main
import "fmt"
func main() {
	const (
		a = iota
		b
		c		
		d = "aiwin"
		e		//未被定义所以用上一个的值,但是iota还是会一直计数
		f = 100
		g 		//未被定义所以用上一个的值
		h = iota
		j
	)
	const (
		k = iota	//新的const出现,新iota被重新从0开始计数
		l
	)
	fmt.Println(a, b, c, d, e, f, g, h, j, k, l) //0 1 2 aiwin aiwin 100 100 7 8 0 1
}
数据类型
-  布尔型 bool,默认值是false
-  数字类型,分为 int和float,并且支持复数,位运算采用补码序号 类型和描述 1 uint8无符号8位整型(0~255) 2 uint16无符号16位整型(0~65535) 3 uint32无符号32位整型(0~4294967295) 4 uint64无符号64位整型(0~18446744073709551615) 5 int8有符号8位整型(-128~127)i 6 int16符号16位整型(-32768~32767) 7 int32有符号32位整型(-2147483648~2147483647) 8 int64有符号64位整型(-9223372036854775808~9223372036854775807) 
-  float浮点型,默认是64位,保留6位小数,保留小数会丢失精度,采取四舍五入的原则序号 类型和描述 1 float32 IEEE-754 32位浮点型数 2 float64 IEEE-754 64位浮点型数 3 complex64 32 位实数和虚数 4 complex128 64 位实数和虚数 
-  类型别名,Go语言会有一些类型的别名序号 类型和描述 1 byte类似uint8 2 int和uint一样大小 3 rune类似int32 7 uintptr无符号整型,用于存放指针 
-  字符类型,’ 和**"** 和双引号包裹的字符是有差别的,’ 默认是int32 类型,会自动转换成Unicode 编码的值,字符可以直接使用**+** 连接
-  类型转换,Go语言不存在隐式类型转换,所有的类型转换都必须是显式的声明
package main
import (
	"fmt"
)
func main() {
	var age byte = 18 //相当于uint8
	//超过范围,报错 age = 9223372036854775808
	var num1 float32 = -123.0000901
	var num2 float64 = -123.0000901
	fmt.Println("num1=", num1, "num2=", num2) //精度缺失
	fmt.Println("转换后导致的精度丢失:num=", float32(num2))
	var num3 float64 = 3.19
	fmt.Printf("num3=%.1f\n", num3) //四舍五入,输出3.2
	fmt.Printf("age的类型为%T,数值为%d", age, age)
	str := "Hello"
	str1 := '中' //使用Unicode编码表,会自动认为是int32类型
	str2 := "World"
	fmt.Printf("%T,%s\n", str, str)
	fmt.Printf("%T,%d\n", str1, str1) //默认是int32类型,转换成数字
	fmt.Println(str + "," + str2)
	var b uint16 = 256
	fmt.Println("b=", uint8(b)) //变成了0
	/*
		flag := 2
		fmt.Println(bool(flag)) 整型不能转换成bool类型*/
}
运算符
| 运算符 | 描述 | 
|---|---|
| && | 逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。 | 
| || | 逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。 | 
| ! | 逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 | 
| 运算符 | 描述 | 
|---|---|
| & | 按位与运算符"&"是双目运算符。都是1结果为1,否则为0 | 
| | | 按位或运算符"|"是双目运算符。 都是0结果为0,否则为1 | 
| ^ | 按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 | 
| << | 左移运算符"<<“是双目运算符。左移n位就是乘以2的n次方。 其功能把”<<“左边的运算数的各二进位全部左移若干位,由”<<"右边的数指定移动的位数,高位丢弃,低位补0。 | 
| >> | 右移运算符">>“是双目运算符。右移n位就是除以2的n次方。 其功能是把”>>“左边的运算数的各二进位全部右移若干位,”>>"右边的数指定移动的位数。 | 
| &^ | 位清空,a&^b,对于b上的每个数值,如果为0,则取a对应位上的数,如果为1,则取0 | 
| 运算符 | 描述 | 
|---|---|
| & | 返回变量存储地址 | 
| * | 指针变量。 | 
package main
import "fmt"
func main() {
	var a uint = 13     //0011 1100
	var b uint = 60     //0000 1101
	fmt.Println(a & b)  //0000 1100
	fmt.Println(a | b)  //0011 1101
	fmt.Println(a ^ b)  //0011 0001
	fmt.Println(a &^ b) // 0000 0001
	fmt.Println(b >> a) //60右移60位,结果为0
}
fmt库
常用的一些函数
- Print/- Println/- Printf:格式化并输出到标准输出。
- Println:类似于- Print,但在输出后添加换行符。
- Printf:使用格式化字符串进行输出(类似于 C 语言中的- printf函数)。
- Sprint/- Sprintln/- Sprintf:将格式化的结果以字符串形式返回,而不是输出到标准输出。
- Fprint/- Fprintln/- Fprintf:将格式化的结果输出到指定的文件(- io.Writer)。
- Errorf:生成一个格式化的错误字符串。
- Scan/- Scanln/- Scanf:从标准输入读取并格式化输入。
- Sscan/- Sscanln/- Sscanf:从给定的字符串中读取并格式化输入。
- Fscan/- Fscanln/- Fscanf:从指定的文件(- io.Reader)中读取并格式化输入。
package main
import (
	"fmt"
	"os"
)
func main() {
	name := "Alice"
	age := 30
	height := 1.68
	// 格式化并输出到标准输出
	fmt.Print("Hello, ")
	fmt.Print(name)
	fmt.Println("!")
	// 使用格式化字符串进行输出
	fmt.Printf("%s is %d years old.\n", name, age)
	// 输出到指定文件
	file, _ := os.Create("user.gob")
	defer file.Close()
	fmt.Fprintln(file, name)
	fmt.Fprintf(file, "%d", age)
	
	//标准化读取文件输入
	file, _ = os.Open("user.gob")
	defer file.Close()
	var ReadName string
	var ReadAge int
	fmt.Fscanln(file, &ReadName) //以行为单位来读取
	fmt.Fscanln(file, &ReadAge)
	fmt.Printf("读取到的数据为,Name: %s,Age: %d\n", ReadName, ReadAge)
	// 将格式化的结果以字符串形式返回
	info := fmt.Sprintf("Name: %s, Age: %d, Height: %.2f", name, age, height)
	fmt.Println(info)
	// 从标准输入读取并格式化输入
	var input string
	fmt.Print("Enter your name: ")
	fmt.Scanln(&input)
	fmt.Printf("Hello, %s!\n", input)
}
流程控制
if、switch、select
| 语句 | 描述 | 
|---|---|
| if语句 | if 语句 由一个布尔表达式后紧跟一个或多个语句组成。 | 
| if else | if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。 | 
| else if | 你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。 | 
| switch | switch 语句用于基于不同条件执行不同动作。 | 
| fallthrough | 当使用swich语句时,可以使用fallthrough 进行case穿透,下面的条件一定会执行 | 
| select | select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。 | 
package main
import (
	"fmt"
	"time"
)
func main() {
	//if语句
	var password string
	var username string
	fmt.Print("请输入账号:")
	fmt.Scan(&username)
	fmt.Print("请输入密码:")
	fmt.Scan(&password)
	if username == "admin" {
		if password == "Qyx@Sxf715" {
			fmt.Println("登录成功")
		} else {
			fmt.Println("密码错误")
		}
	} else {
		fmt.Println("用户名错误") //注意这里的else一定要接在if的}后面,不能进行换行
	}
	//switch
	var score int = 88
	switch {
	case score >= 90:
		fmt.Println("成绩为A级")
	case score >= 80 && score < 90:
		fmt.Println("成绩为B级")
		//fallthrough 一定会把下面的一个case也穿透掉
	case score >= 70 && score < 80:
		fmt.Println("成绩为C级")
	case score >= 60 && score < 70:
		fmt.Println("成绩为D级")
	default:
		fmt.Println("成绩为不及格")
	}
	//select语句的使用
	ch1 := make(chan string) //创建了两个通道 ch1 和 ch2
	ch2 := make(chan string)
	//两个匿名的 goroutine 分别向这两个通道发送值
	go func() {
		time.Sleep(2 * time.Second)
		ch1 <- "Hello"
	}()
	go func() {
		time.Sleep(3 * time.Second)
		ch2 <- "World"
	}()
	/*select 会同时监听多个通道的操作,当任何一个 case 中的操作就绪时,该 case 就会被执行。
	如果同时有多个 case 就绪,select 随机选择一个可执行的 case 来执行。
	如果没有任何 case 就绪,并且存在 default 分支,那么执行 default 分支。*/
	select {
	case msg1 := <-ch1:
		fmt.Println("Received from ch1:", msg1)
	case msg2 := <-ch2:
		fmt.Println("Received from ch2:", msg2)
	case <-time.After(5 * time.Second):
		fmt.Println("Timed out")
	}
}
for、break、continue
Go语言的for循环也需要三个参数,起始位,最终位,间距 ,但是三个参数都可以省略掉。
package main
import "fmt"
func main() {
	//9*9乘法表
	for j := 1; j <= 9; j++ {
		for i := 1; i <= j; i++ {
			fmt.Printf("%dx%d=%d \t", i, j, i*j)
		}
		fmt.Println()
	}
	for j := 1; j <= 9; j++ {
		if j == 5 {
			//结束掉整个循环
			break
		}
		fmt.Print(j)
	}
	fmt.Println()
	for j := 1; j <= 9; j++ {
		if j == 5 {
			//结束当次循环
			continue
		}
		fmt.Print(j)
	}
}
遍历String
package main
import "fmt"
func main() {
	var str string = "Hello,Aiwin"
	fmt.Println(str)
	fmt.Printf("字符串的长度为%d\n", len(str))
	fmt.Printf("第二个字符是%c\n", str[1])
	//遍历字符串
	for i := 0; i < len(str); i++ {
		fmt.Printf("%c", str[i])
	}
	//for range
	fmt.Println("\n")
	for i, v := range str {
		fmt.Printf("%d%c\n", i, v)
	}
}
函数
- 函数是一个基本代码块,用于执行一个任务
- Go语言最少有一个main函数
- 函数声明告诉编译器函数的名称,返回类型,参数
- 函数本身也是一个变量,也可以进行赋值
function 函数名(参数,参数类型)(返回类型){}
- 形式参数:定义函数时,用于接收外部传入数据的参数
- 实际参数:调用函数时,传给形参的实际数据是实际参数
- 可变参数:参数类型确定,但是数量不确定,可以用**…,可变参数前面可以继续定义参数,后面不能再定义参数,一个函数列表只能有一个**可变参数
package main
import (
	"fmt"
)
func main() {
	fmt.Println("1+2的结果为", add(1, 2))
	x, y := swap("你好", "Go语言")
	fmt.Println(x, y)
	printMessage("一个参数的函数")
	printStatic()
	fmt.Println("求和函数:", getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
}
// 有多个返回值函数
func swap(x, y string) (string, string) {
	return y, x
}
// 有两个参数的函数
func add(a, b int) int {
	result := a + b
	return result
}
// 一个参数函数
func printMessage(msg string) {
	fmt.Println(msg)
}
// 无参数函数
func printStatic() {
	fmt.Println("无参数函数")
}
func getSum(number ...int) int {
	sum := 0
	for i := 0; i < len(number); i++ {
		sum += number[i]
	}
	return sum
}
值传递和引用传递
- 值类型数据:操作的是数据本身,如int,string ,bool,array,改变值不会改变数据本身,地址是不一样的
- 引用类型数据,操作的是数据的地址,如slice,map ,chanel,改变的时候会一起改变,函数地址是一样的。
defer
可以在函数中添加多个defer语句,当函数执行到最后时,这些defer语句会逆序执行,可以用于在函数返回前关闭相应的资源等操作
package main
import "fmt"
func main() {
	//值传递
	arr := [4]int{1, 2, 3, 4}
	updateArr(arr)
	fmt.Println(arr)
	//引用传递
	sli := []int{1, 2, 3, 4, 5}
	updateSlice(sli)
	fmt.Println(sli)
	//defer函数
	a := 10
	defer MyPrint(a) //输出10,已经传递进去了,一切准备就绪
	a++
	fmt.Println(a)
}
func updateArr(arr [4]int) {
	arr[1] = 100
	fmt.Println(arr)
}
func updateSlice(sli []int) {
	sli[1] = 100
	fmt.Println(sli)
}
func MyPrint(number int) {
	fmt.Println(number)
}
init
-  **init()**函数不能被其它函数调用,而是在main函数执行之前自动被调用 
-  init 函数不能作为参数传入,不能有传入参数和返回值 
-  当一个main有多个init函数,谁在前谁就先执行 
匿名、回调、闭包函数
- 也叫闭包函数(closures),允许临时创建一个没有指定名称的函数
- 回调函数就是一个函数作为另一个函数的参数。
- 闭包结构,一个外层函数中有内层函数,该内层函数中可以操作外层函数的局部变量,并且外层函数的返回值是内层函数,这种结构是闭包结构。
- 正常的局部变量会随着函数的调用而创建,随着函数的结束而销毁,但是在闭包结构中的外层函数的局部变量并不会随着外层函数的结果而销毁,因为内层函数还在堆栈中使用
package main
import "fmt"
func main() {
	r1 := operator(1, 2, add)
	fmt.Println(r1)
	//匿名函数
	r2 := operator(3, 2, func(a, b int) int {
		if b == 0 {
			return 0
		}
		return a / b
	})
	fmt.Println(r2)
	//闭包结构
	f1 := increment()
	fmt.Println(f1())
	fmt.Println(f1())
	f2 := increment()
	fmt.Println(f2())
	fmt.Println(f1())//输出3,f1没有被销毁
}
// 回调函数,fun是告诫函数,operator是回调函数
func operator(a, b int, fun func(int, int) int) int {
	return fun(a, b)
}
func add(a, b int) int {
	return a + b
}
func increment() func() int {
	i := 0
	fun := func() int {
		i++
		return i
	}
	return fun
}
数组和切片
数组的数量在创建的时候就定义的,不可再进行改变。
数组的定义语法:
var 数组名 [数量]数据类型=[数量]数据类型{数据}
切片相对于数组来说更加灵活,切片的长度是可变的,相当于可切的数组
package main
import "fmt"
func main() {
	var nameArr [3]string = [3]string{"Hello", "Wor", "ld"}
	fmt.Println(nameArr)
	var nameList []string
	nameList = append(nameList, "Hello")
	fmt.Println(nameList)
}
Map
Map就是Python中的字典(键值对),Map创建的时候一定需要初始化
var 变量名 map[键类型]值类型{}
package main
import "fmt"
func main() {
	var userMap map[int]string = map[int]string{
		1: "Aiwin",
		2: "Lau",
		3: "",
	}
	fmt.Println(userMap[1])
	value, ok := userMap[3] //3存在,ok是true
	fmt.Println(value, ok)
	userMap[1] = "LauAiwin"
	delete(userMap, 3)
	fmt.Println(userMap)
}
结构体
结构体定义,可以类比为对象
type 结构体名称 struct{
	名称 类型
}
package main
import (
	"encoding/json"
	"fmt"
)
type Parent struct {
	Name string
	Age  int
}
type Children struct {
	Parent
	Name string
	Age  int
}
func (children *Children) setChildernName(name string, age int) { //引用传递
	children.Name = name
	children.Age = age
}
type User struct {
	Username string `json:"username"`      //转换结果为username
	Password string `json:"-"`             //不显示
	Age      int    `json:"age,omitempty"` //抛弃空值
}
func main() {
	parent := Parent{Name: "Lau", Age: 40}
	childern := Children{parent, "Aiwin", 20}
	fmt.Printf("%s的父亲是:%s,年龄是:%d\n", childern.Name, childern.Parent.Name, childern.Parent.Age)
	childern.setChildernName("LauAiwin", 19)
	fmt.Println(childern)
	user := User{"Aiwin", "123456", 0}
	byteData, _ := json.Marshal(user)
	fmt.Println(string(byteData))
}
自定义数据类型
自定义类型的本意就是为了代码更简化、易于理解、方便维护。
类型别名(就是将类型赋值给一个type)
- 不能绑定方法
- 打印类型还是原始类型
- 类型别名不用转换
package main
import "fmt"
type Code int
const (
	SuccessCode Code = 1
	ErrorCode   Code = 2
)
func (code Code) getMessage() (message string) {
	switch code {
	case SuccessCode:
		return "请求成功"
	case ErrorCode:
		return "请求失败"
	}
	return ""
}
func (code Code) result() (result Code, message string) {
	return code, code.getMessage()
}
func Request(name string) (code Code, message string) {
	if name == "admin" {
		return SuccessCode.result()
	} else {
		return ErrorCode.result()
	}
}
func main() {
	fmt.Println(Request("admin"))
}
接口
接口是一组仅包含方法名、参数、返回值的为具体实现的方法的集合,同样接口也不能绑定方法
package main
import "fmt"
type Student struct {
	name string
}
type Name interface {
	getName() string
}
func (student Student) getName() string {
	return student.name
}
type Teacher struct {
	name string
}
func (teacher Teacher) getName() string {
	return teacher.name
}
// 接口,可以统一传入其它的类型
func getName(name Name) string {
    //name.(Teacher)
	switch types := name.(type) { //类型断言
	case Teacher:
		fmt.Println(types)
	case Student:
		fmt.Println(types)
	}
	return name.getName()
}
func MyPrint(val interface{}) { //空接口
	fmt.Println(val)
}
func main() {
	student := Student{"Aiwin"}
	teacher := Teacher{"Xd"}
	fmt.Println(getName(student))
	fmt.Println(getName(teacher))
	MyPrint(1)
}
协程和channel
协程可以理解为轻量级线程,一个线程可以拥有多个协程,与线程相比,协程不受操作系统调度,协程调度器按照调度策略把协程调度到线程中执行,协程调度器由应用程序的runtime包提供,用户使用go关键字即可创建协程,这也就是GO在语言层面直接支持协程的特色
package main
import (
	"fmt"
	"sync"
	"time"
)
func shopping(name string, group *sync.WaitGroup) {
	fmt.Printf("%s 开始攻击\n", name)
	time.Sleep(1 * time.Second)
	fmt.Printf("%s 停止攻击\n", name)
	group.Done()
}
func main() {
	var group sync.WaitGroup
	StartTime := time.Now()
	group.Add(3)
	go shopping("张三", &group)
	go shopping("李四", &group)
	go shopping("王五", &group)
	group.Wait()
	fmt.Println(time.Since(StartTime))
}
那么协程里面产生的数据,怎么传递给主线程,Go 官方使用channel 来传递
package main
import (
	"fmt"
	"sync"
	"time"
)
var moneyChanel chan int = make(chan int)
var nameChanel chan string = make(chan string)
var DoneChanel chan struct{} = make(chan struct{})
func shopping(name string, money int, group *sync.WaitGroup) {
	fmt.Printf("%s 开始攻击\n", name)
	time.Sleep(1 * time.Second)
	fmt.Printf("%s 停止攻击\n", name)
	moneyChanel <- money
	nameChanel <- name
	group.Done()
}
func main() {
	var group sync.WaitGroup
	StartTime := time.Now()
	group.Add(3)
	go shopping("张三", 100, &group)
	go shopping("李四", 150, &group)
	go shopping("王五", 160, &group)
	var moneyList []int
	var nameList []string
	go func() { //解决moneyChanel一直死循环的问题
		defer close(moneyChanel)
		defer close(nameChanel)
		defer close(DoneChanel)
		group.Wait()
	}()
	//go func(){
	//	for money := range moneyChanel {
	//		moneyList = append(moneyList, money) //解决moneyChannel被一直输数据问题
	//	}
	//}()
	//for name := range nameChanel {
	//	nameList = append(nameList, name)
	//}
	event := func() {
		for {
			select {
			case names := <-nameChanel:
				nameList = append(nameList, names)
			case money := <-moneyChanel:
				moneyList = append(moneyList, money)
			case <-DoneChanel://解决当协程全部完事,退出循环的问题
				return
			}
		}
	}
	event()
	fmt.Println(moneyList)
	fmt.Println(nameList)
	fmt.Println(time.Since(StartTime))
}
超时
package main
import (
	"fmt"
	"time"
)
var doneChanel = make(chan struct{})
func timeOut() {
	fmt.Println("开始")
	time.Sleep(3 * time.Second)
	fmt.Println("结束")
	close(doneChanel)
}
func main() {
	go timeOut()
	select {
	case <-doneChanel:
		fmt.Printf("执行完成")
	case <-time.After(4 * time.Second):
		fmt.Println("超时")
		return
	}
}
线程锁
package main
import (
	"fmt"
	"sync"
)
var sum int
var wait sync.WaitGroup
var lock sync.Mutex
func add() {
	lock.Lock() //线程锁,不然会线程紊乱
	for i := 0; i < 10000; i++ {
		sum++
	}
	lock.Unlock()
	wait.Done()
}
func sub() {
	lock.Lock() 
	for i := 0; i < 10000; i++ {
		sum--
	}
	lock.Unlock()
	wait.Done()
}
func main() {
	wait.Add(2)
	go add()
	go sub()
	wait.Wait()
	fmt.Println(sum)
	var maps = sync.Map{}//Map的协程紊乱,要使用这种创建方式
	go func() {
		for {
			maps.Store(1, "Aiwin")
		}
	}()
	go func() {
		for {
			fmt.Println(maps.Load(1))
		}
	}()
	select {}
}
异常处理
Go语言没有捕获异常的机制,每次都要接error ,这是Go语言的一个诟病,异常处理可分为三种,分别是中断、恢复、从上一级返回处理。
例子:
package main
import (
	"errors"
	"fmt"
)
// 中断仅适用于init开始时
//
//	func init() {
//		_, err := os.ReadFile("aaa")
//		if err != nil {
//			panic("中断报错了")
//		}
//	}
func div(a, b int) (res int, err error) {
	if b == 0 {
		err = errors.New("除数不能为0")
		return 0, err
	}
	res = a / b
	return res, nil
}
func zhixing() (res int, err error) {
	res, err = div(2, 0)
	if err != nil {
		return 0, err
	}
	res += 2
	return res, nil
}
func recovery() {
	defer func() {
		recover()
	}()
	var lists []int = []int{1, 2, 3}
	fmt.Println(lists[4])
}
func main() {
	/*错误向上处理
	res, err := zhixing()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(res)*/
	recovery()
	fmt.Println("恢复正常逻辑")
}
泛型
Go语言的泛型是指在定义函数、数据结构或接口时,可以不指定具体数据类型,而是以一种通用的方式编写代码,以便在不同的数据类型上有效地进行操作。泛型使得代码更具有通用性和可复用性,因为它可以适用于多种不同类型的数据而无需重复编写相似的代码。
比如说结构的泛型:
package main
import (
	"encoding/json"
	"fmt"
)
type Response[A any] struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data A      `json:"data"`
}
type User struct {
	Name string `json:"name"`
}
type User1 struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}
func main() {
	//UserInfo := Response[User]{
	//	Code: 1,
	//	Msg:  "反序列化",
	//	Data: User{
	//		Name: "Aiwin",
	//	},
	//}
	//marshal, _ := json.Marshal(UserInfo)
	//fmt.Println(string(marshal))
	var UnMarRes Response[User]
	json.Unmarshal([]byte(`{"code":1,"msg":"反序列化","data":{"name":"Aiwin"}}`), &UnMarRes) //通过泛型可以识别是属于哪一个User
	fmt.Println(UnMarRes.Data.Name)
}
文件读取
-  os.ReadFile()一次性读取整个文件的内容。
-  os.Open()分片读取。
-  bufio依赖来读取,指定分割符,换行符等package main import ( "bufio" "fmt" "os" ) func main() { file, err := os.Open("text.txt") if err != nil { panic("文件读取错误") } //buf := bufio.NewReader(file) //for { // line, _, err := buf.ReadLine() // if err == io.EOF { // break // } // fmt.Println(string(line)) //} //指定分割符 scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) for scanner.Scan() { fmt.Println(scanner.Text()) } }
文件写入
-  os.openFile()中flag的类型:const ( O_RDONLY int = syscall.O_RDONLY // open the file read-only. O_WRONLY int = syscall.O_WRONLY // open the file write-only. O_RDWR int = syscall.O_RDWR // open the file read-write. O_APPEND int = syscall.O_APPEND // append data to the file when writing. O_CREATE int = syscall.O_CREAT // create a new file if none exists. O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist. O_SYNC int = syscall.O_SYNC // open for synchronous I/O. O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened. )例子: package main import ( "fmt" "io" "os" ) func main() { //文件写入 file, err := os.OpenFile("text.txt", os.O_CREATE|os.O_RDWR, 0777) //模式和权限,权限仅针对linux系统 if err != nil { panic("文件写入失败") } defer file.Close() file.Write([]byte("hello")) //文件写入 err1 := os.WriteFile("text.txt", []byte("password"), 0777) //全部写入,会全覆盖 fmt.Println(err1) //文件复制 rFile, err2 := os.Open("text.txt") if err2 != nil { panic("文件不可取") } wFile, err3 := os.OpenFile("copy.txt", os.O_CREATE|os.O_WRONLY, 0777) if err3 != nil { panic("文件错误") } defer wFile.Close() io.Copy(wFile, rFile) //文件复制 dir, err := os.ReadDir("基础语法") if err != nil { panic("error") } for _, entry := range dir { info, _ := entry.Info() fmt.Println(entry.IsDir(), entry.Name(), info.Size()) } }反射- reflect.typeof获取类型
- reflect.Valueof获取值
- setInt、setString重新设置值
 
demo
package main
import (
	"fmt"
	"reflect"
)
func getType(obj any) {
	v := reflect.TypeOf(obj)
	switch v.Kind() {
	case reflect.Int:
		fmt.Println("获取到Int类型")
	case reflect.String:
		fmt.Println("获取到String类型")
	}
}
//注意要更改值,需要使用指针的形式更改
func setValue(obj any, value any) {
	v1 := reflect.ValueOf(obj)
	v2 := reflect.ValueOf(value)
	if v1.Elem().Kind() != v2.Kind() { //获取一个值的指针所指向的元素值
		return
	}
	switch v1.Elem().Kind() {
	case reflect.Int:
		v1.Elem().SetInt(v2.Int())
	case reflect.String:
		v1.Elem().SetString(value.(string))
	}
}
func main() {
	var name = "张三"
	var age = 24
	getType(name)
	getType(age)
	setValue(&name, "李四")
	setValue(&age, 25)
	fmt.Println(name, age)
}
package main
import (
	"fmt"
	"reflect"
)
type User struct {
	UserName string `json:"userName"`
	Password string `json:"password"`
}
func demo(obj any) {
	t := reflect.TypeOf(obj)
	v := reflect.ValueOf(obj)
	for i := 0; i < t.NumField(); i++ {
		value := v.Field(i)
		fmt.Println(value)
	}
	for i := 0; i < t.NumMethod(); i++ {
		m := t.Method(i)
		if m.Name != "Call" { //方法名必须为大写
			continue
		}
		method := v.Method(i)
		method.Call([]reflect.Value{
			reflect.ValueOf("Aiwin"),
		})
	}
}
func (User) Call(name string) {
	fmt.Println("我的名字是", name)
}
func main() {
	user := User{UserName: "Aiwin", Password: "123456"}
	demo(user)
}
反射转ord小案例:
package main
import (
	"errors"
	"fmt"
	"reflect"
	"strings"
)
type Users struct {
	Name string `orm:"name"`
	Id   int    `orm:"id"`
}
func Find(obj any, query ...any) (sql string, err error) {
	t := reflect.TypeOf(obj)
	if t.Kind() != reflect.Struct {
		err = errors.New("非结构体")
		return
	}
	//获取到where
	if len(query) > 0 {
		// 有第二个参数,校验第二个参数中的?个数,是不是和后面的个数一样
		q := query[0] //取第一个参数
		if strings.Count(q.(string), "?")+1 != len(query) {
			err = errors.New("参数个数不对")
			return
		}
		var where string
		for _, a := range query[1:] {
			at := reflect.TypeOf(a)
			switch at.Kind() {
			case reflect.Int:
				q = strings.Replace(q.(string), "?", fmt.Sprintf("%d", a.(int)), 1)//将?替换成数值
			case reflect.String:
				q = strings.Replace(q.(string), "?", fmt.Sprintf("'%s'", a.(string)), 1)
			}
		}
		where += "where " + q.(string)
		//获取到字段
		var columns []string
		for i := 0; i < t.NumField(); i++ {
			field := t.Field(i)
			f := field.Tag.Get("orm")
			columns = append(columns, f)
		}
		//获取表名称
		table := strings.ToLower(t.Name())
		sql = fmt.Sprintf("select %s from %s %s", strings.Join(columns, ","), table, where)
	}
	return
}
func main() {
	sql, err := Find(Users{}, "name= ? and id = ?", "Aiwin", 1)
	//select name,id from Users where name='Aiwin' and id=1
	fmt.Println(sql, err)
	sql, err = Find(Users{}, "id = ?", 1)
	//select name,id from Users where id=1
	fmt.Println(sql, err)
}
TCP网络编程
主要通过net依赖包来完成,比如以下方法:
- net.Dial(network, address string) (Conn, error):通过指定的网络协议和地址连接到远程主机,返回一个Conn接口类型的实例和可能的错误。
- net.Listen(network, address string) (Listener, error):在指定的网络协议和地址上监听连接,返回一个Listener接口类型的实例和可能的错误。
- net.DialTimeout(network, address string, timeout time.Duration) (Conn, error):在指定的超时时间内,通过指定的网络协议和地址连接到远程主机,返回一个Conn接口类型的实例和可能的错误。
- net.ListenPacket(network, address string) (PacketConn, error):在指定的网络协议和地址上监听数据包的到达,返回一个PacketConn接口类型的实例和可能的错误。
- net.ResolveTCPAddr(network, address string) (*TCPAddr, error):将字符串形式的TCP地址解析为TCPAddr类型的实例,包括IP地址和端口号等信息。
- net.ResolveUDPAddr(network, address string) (*UDPAddr, error):将字符串形式的UDP地址解析为UDPAddr类型的实例,包括IP地址和端口号等信息。
- net.LookupHost(host string) ([]string, error):通过主机名查询对应的IP地址列表,并返回一个字符串切片和可能的错误。
- net.LookupPort(network, service string) (port int, err error):通过网络协议和服务名查询对应的端口号,并返回端口号和可能的错误。
demo:
package main
import (
	"fmt"
	"io"
	"net"
)
func main() {
	tcp, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:81") //将一个字符串形式的 TCP 地址解析为一个 TCPAddr 类型的实例
	listen, _ := net.ListenTCP("tcp", tcp)              //进行监听
	for {
		//接收连接
		fmt.Println("开始监听....")
		con, err := listen.Accept()
		if err != nil {
			break
		}
		fmt.Println(con.RemoteAddr().String() + "进来了")
		for {
			var buf []byte = make([]byte, 1024)
			n, err := con.Read(buf)
			//客户端退出
			if err == io.EOF {
				fmt.Println(con.RemoteAddr().String() + "退出了")
				break
			}
			fmt.Println(string(buf[0:n]))
		}
	}
}
package main
import (
	"fmt"
	"net"
)
func main() {
	conn, _ := net.Dial("tcp", "127.0.0.1:81")
	var s string
	for {
		fmt.Scanln(&s)
		if s == "quit" {
			break
		}
		conn.Write([]byte(s))
	}
	conn.Close()
}
Http
- http.HandleFunc(pattern string, handler func(ResponseWriter, *Request)):注册一个处理函数,用于指定URL模式的请求处理。
- http.Handle(pattern string, handler http.Handler):注册一个处理器对象,该对象实现了http.Handler接口,用于指定URL模式的请求处理。
- http.ListenAndServe(addr string, handler http.Handler):启动一个HTTP服务器,监听指定地址,并使用指定的处理器对象处理接收到的请求。
- http.Get(url string) (resp *http.Response, err error):向指定URL发起GET请求,并返回响应结果。
- http.Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error):向指定URL发起POST请求,携带指定类型的请求体,并返回响应结果。
- http.NewRequest(method, url string, body io.Reader) (*Request, error):创建一个自定义的HTTP请求对象,可以指定请求方法、URL和请求体。
- http.DefaultServeMux:默认的ServeMux多路复用器,可以通过它来注册处理函数,用于处理HTTP请求。
- http.HandlerFunc:将一个函数转换为http.Handler接口的实现,用于处理HTTP请求。
- http.Error(w ResponseWriter, error string, code int):向客户端发送指定状态码的错误响应。
- http.Redirect(w ResponseWriter, r *Request, url string, code int):向客户端发送重定向指令,使其跳转到指定的URL。
demo
package main
import (
	"crypto/md5"
	"fmt"
	"net/http"
)
func HashUsingMD5(input string) string {
	hasher := md5.New()
	hasher.Write([]byte(input))
	return fmt.Sprintf("%x", hasher.Sum(nil))
}
func LoginHandler(res http.ResponseWriter, req *http.Request) {
	if req.Method == "POST" {
		username := req.FormValue("username")
		password := req.FormValue("password")
		if username == "admin" && password == "123456" {
			Cookie := HashUsingMD5(username + password)
			cookie := http.Cookie{
				Name:  "Value",
				Value: Cookie,
			}
			http.SetCookie(res, &cookie)
			http.Redirect(res, req, "/success", http.StatusFound)
		} else {
			http.Redirect(res, req, "/", http.StatusFound)
		}
	} else {
		res.WriteHeader(405)
		res.Write([]byte("Method Not Allow"))
	}
}
func SuccessHandler(res http.ResponseWriter, req *http.Request) {
	cookie, err := req.Cookie("Value")
	if err != nil {
		http.Redirect(res, req, "/", http.StatusFound)
		return
	}
	cookie_value := HashUsingMD5("admin123456")
	if cookie.Value != cookie_value {
		http.Redirect(res, req, "/", http.StatusFound)
	} else {
		res.Write([]byte("登录成功"))
	}
}
func main() {
	//创建一个文件服务器来处理静态文件
	fs := http.FileServer(http.Dir("C:\\Users\\25018\\GolandProjects\\Project\\网络编程\\http"))
	http.Handle("/", http.StripPrefix("/", fs))
	http.HandleFunc("/login", LoginHandler)
	http.HandleFunc("/success", SuccessHandler)
	fmt.Println("HTTP server running at http://127.0.0.1:7000")
	err := http.ListenAndServe("127.0.0.1:7000", nil)
	if err != nil {
		fmt.Println(err)
	}
}
package main
import (
	"fmt"
	"net/http"
	"net/url"
)
func main() {
	client := &http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	res, err := client.PostForm("http://127.0.0.1:7000/login", url.Values{"username": {"admin"}, "password": {"123456"}})
	if err != nil {
		fmt.Println("请求失败")
	}
	if res.StatusCode == http.StatusFound {
		fmt.Println(res.Header)
		fmt.Println("重定向成功")
	}
}
websocket
go get github.com/gorilla/websocket
websocket是socket连接和http协议的结合体,可以实现网页和服务端的长连接
demo
package main
import (
	"fmt"
	"github.com/gorilla/websocket"
	"net/http"
)
var UP = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}
func handler(res http.ResponseWriter, req *http.Request) {
	conn, err := UP.Upgrade(res, req, nil)
	if err != nil {
		fmt.Println(err)
		return
	}
	for {
		//消息类型,消息,错误
		types, message, err := conn.ReadMessage()
		if err != nil {
			break
		}
		conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("你说的是:%s吗?", string(message))))
		fmt.Println(types, string(message))
	}
	defer conn.Close()
	fmt.Println("服务关闭")
}
func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe("127.0.0.1:7000", nil)
}
package main
import (
	"bufio"
	"fmt"
	"github.com/gorilla/websocket"
	"os"
)
func send(conn *websocket.Conn) {
	reader := bufio.NewReader(os.Stdin)
	l, _, _ := reader.ReadLine()
	conn.WriteMessage(websocket.TextMessage, l)
}
func main() {
	dl := websocket.Dialer{}
	conn, _, err := dl.Dial("ws://127.0.0.1:7000", nil)
	if err != nil {
		fmt.Println(err)
		return
	}
	for {
		go send(conn)
		t, p, err := conn.ReadMessage()
		if err != nil {
			break
		}
		fmt.Println(t, string(p)) //1代表TextMessage
	}
}
爬虫
正则表达式
package main
import (
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"io"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"
	"xorm.io/xorm"
)
type Page struct {
	Id          int64 `xorm:"pk autoincr"` //Id自增
	Title       string
	Content     string    `xorm:"text"`
	CreateTime  time.Time `xorm:"created"`
	UpdatedTime time.Time `xorm:"updated"`
}
var engine *xorm.Engine
func init() {
	dbType := "mysql"
	dbHost := "localhost"
	dbPort := "3306"
	dbUser := "root"
	dbPassword := "root"
	dbName := "test_xorm"
	// 创建引擎
	var err error
	engine, err = xorm.NewEngine(dbType, dbUser+":"+dbPassword+"@tcp("+dbHost+":"+dbPort+")/"+dbName) //连接mysql数据库
	//engine, err = xorm.NewEngine("mysql", "root:root@/test_xorm?charset=utf-8") //连接mysql数据库
	if err != nil {
		fmt.Println(err)
		panic("初始化不成功")
	} else {
		err2 := engine.Ping()
		if err2 != nil {
			panic("连接不成功")
		} else {
			fmt.Println("连接成功!")
		}
	}
}
func fetch(url string) string {
	client := &http.Client{}
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36")
	req.Header.Add("Cookie", "_gid=GA1.2.1321863478.1703508772; _ga_YXBYDX14GJ=GS1.1.1703508771.1.1.1703510246.58.0.0; _ga=GA1.2.1396264026.1703508772")
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Http error", err)
		return ""
	}
	if resp.StatusCode != 200 {
		fmt.Println("Http Status Code", resp.StatusCode)
		return ""
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Read error", err)
		return ""
	}
	return string(body)
}
func parse(html string) {
	html = strings.Replace(html, "\n", "", -1)
	re_sidebar := regexp.MustCompile(`<aside id="sidebar" role="navigation">(.*?)</aside>`)
	sidebar := re_sidebar.FindString(html)
	re_link := regexp.MustCompile(`href="(.*?)"`)
	links := re_link.FindAllString(sidebar, -1)
	url := "https://gorm.io/zh_CN/docs/"
	for _, value := range links {
		fmt.Println(value)
		href := value[6 : len(value)-1]
		url := url + href
		fmt.Println("url\n", url)
		body := fetch(url)
		go parse2(body)
	}
}
func parse2(body string) {
	body = strings.Replace(body, "\n", "", -1)
	re_title := regexp.MustCompile(`<h1 class="article-title" itemprop="name">(.*?)</h1>`)
	title := re_title.FindString(body)
	title = title[42 : len(title)-5]
	fmt.Println("title", title)
	//save(title, body)
	saveToDB(title, body)
}
func save(title string, content string) {
	err := os.WriteFile("网络编程/Go爬虫/pages/"+title+".html", []byte(content), 0644)
	if err != nil {
		panic("保存出现错误")
	}
}
func saveToDB(title string, content string) {
	err := engine.Sync(new(Page))
	if err != nil {
		fmt.Println("Failed to sync database: %v", err)
	}
	page := Page{
		Title:   title,
		Content: content,
	}
	affected, err := engine.Insert(&page)
	if err != nil {
		fmt.Println("插入出现错误", err)
	}
	fmt.Println("save:" + strconv.FormatInt(affected, 10))
}
func main() {
	url := "https://gorm.io/zh_CN/docs/"
	html := fetch(url)
	parse(html)
}
goquery
通过goquery可以快速的对HTML或XML进行解析,提供简单的API提取一个HTML或XML页面中的节点。
常用元素:
- Find: 通过CSS选择器查找元素,例如 doc.Find("div.content")将返回所有class为content的div元素。
- Each: 遍历匹配的元素集合,并对每个元素执行指定的函数。
- Text: 获取元素的文本内容。
- Attr: 获取元素的属性值。
- Html: 获取匹配元素的HTML内容。
- Parent, Children, Next, Prev: 获取父元素、子元素、相邻的后一个元素、相邻的前一个元素等。
- Filter, Not, HasClass, Is: 根据特定条件对元素集合进行过滤和判断。
- AddClass, RemoveClass, ToggleClass: 添加、移除、切换元素的类名。
- Serialize: 将匹配的表单元素序列化为URL编码的字符串。
- Each: 遍历匹配的元素集合,并对每个元素执行指定的函数。
package main
import (
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"net/http"
)
func main() {
	url := "https://gorm.io/zh_CN/docs/"
	//goquery.NewDocument(url)已过时弃用
	res, err := http.Get(url)
	if err != nil {
		panic("http请求出现错误")
	}
	defer res.Body.Close()
	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		panic("goquery解析失败")
	}
	doc.Find(".sidebar-link").Each(func(i int, s *goquery.Selection) {
		href, _ := s.Attr("href")
		base_url := "https://gorm.io/zh_CN/docs/"
		detail_url := base_url + href
		resp, _ := http.Get(detail_url)
		detail_doc, _ := goquery.NewDocumentFromReader(resp.Body)
		title := detail_doc.Find(".article-title").Text()
		content, _ := detail_doc.Find(".article").Html()
		fmt.Println("title:\n", title)
		fmt.Println("content\n", content)
	})
}
colly
Colly 是一个用于爬取 Web 数据的 Golang 框架,具有以下特点:
- 简单易用:Colly 提供了一个简洁、直观的 API,易于使用和理解。
- 高度灵活:Colly 允许你自定义请求头、回调函数、处理方法等,以满足各种爬取需求。
- 并发支持:Colly 提供了并发请求的支持,可以同时处理多个请求,提高爬取效率。
- 支持动态网页:Colly 集成了 PhantomJS,可以处理 JavaScript 渲染的动态网页。
- 内置的选择器引擎:Colly 使用自己的选择器引擎,类似于 CSS 选择器,可以轻松地从 HTML 中提取所需的数据。
- 自动处理重试和错误:Colly 可以自动处理请求的重试和错误,提供了一种简化错误处理的方法。
- 支持代理:Colly 允许你使用代理服务器进行请求,以帮助隐藏真实 IP 地址和绕过访问限制。
- 自定义数据存储:Colly 提供了灵活的机制,允许你自定义数据的存储方式,比如输出到文件、存储到数据库等。
- 事件驱动:Colly 采用事件驱动的方式,通过注册回调函数处理请求和提取数据。
- 广泛的社区支持:Colly 作为一个流行的爬虫框架,有着活跃的社区,你可以轻松找到相关的文档、教程和示例代码。
package main
import (
	"fmt"
	"github.com/gocolly/colly"
)
func main() {
	c := colly.NewCollector()
	c.OnHTML(".sidebar-link", func(element *colly.HTMLElement) {
		href := element.Attr("href")
		if href != "index.html" {
			c.Visit(element.Request.AbsoluteURL(href))
		}
	})
	c.OnHTML(".article-title", func(element *colly.HTMLElement) {
		title := element.Text
		fmt.Println("title:", title)
	})
	c.OnHTML(".article", func(element *colly.HTMLElement) {
		content, _ := element.DOM.Html()
		fmt.Println("content:", content)
	})
	c.OnRequest(func(request *colly.Request) {
		fmt.Println(request.URL.String())
	})
	url := "https://gorm.io/zh_CN/docs/"
	c.Visit(url)
}
豆瓣250
package main
import (
	"fmt"
	"github.com/PuerkitoBio/goquery"
	_ "github.com/go-sql-driver/mysql"
	"net/http"
	"regexp"
	"strconv"
	"xorm.io/xorm"
)
type MovieData struct {
	Id       int64 `xorm:"pk autoincr"`
	Title    string
	Year     string
	Score    string
	Director string
	Actor    string `xorm:"text"`
	Quote    string `xorm:"text"`
	Picture  string
}
var engine *xorm.Engine
func init() {
	dbType := "mysql"
	dbHost := "localhost"
	dbPort := "3306"
	dbUser := "root"
	dbPassword := "root"
	dbName := "test_xorm"
	var err error
	engine, err = xorm.NewEngine(dbType, dbUser+":"+dbPassword+"@tcp("+dbHost+":"+dbPort+")/"+dbName)
	if err != nil {
		fmt.Println(err)
		panic("初始化失败")
	} else {
		err2 := engine.Ping()
		if err2 != nil {
			panic("连接不成功")
		} else {
			fmt.Println("连接成功!")
		}
	}
}
func main() {
	for i := 0; i < 10; i++ {
		fmt.Printf("正常爬取第 %d 页信息\n", i)
		Spider(strconv.Itoa(i * 25))
	}
}
func Spider(page string) {
	url := "https://movie.douban.com/top250" + "?start=" + page
	client := http.Client{}
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
	req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36")
	req.Header.Set("Accept-Encoding", "zh-CN,zh;q=0.9")
	req.Header.Set("Cache-Control", "max-age=0")
	resp, _ := client.Do(req)
	doc, _ := goquery.NewDocumentFromReader(resp.Body)
	//#content > div > div.article > ol > li:nth-child(1)
	//#content > div > div.article > ol > li:nth-child(2)
	doc.Find("#content > div > div.article > ol > li").Each(func(i int, selection *goquery.Selection) {
		title := selection.Find("div > div.info > div.hd > a > span:nth-child(1)").Text()
		img := selection.Find("div > div.pic > a > img")
		imgUrl, ok := img.Attr("src")
		quote := selection.Find("div > div.info > div.bd > p.quote > span").Text()
		score := selection.Find("div > div.info > div.bd > div > span.rating_num").Text()
		info := selection.Find("div > div.info > div.bd > p:nth-child(1)").Text()
		if ok {
			year, director, actor := InfoHandler(info)
			saveToDb(title, imgUrl, year, score, quote, director, actor)
		}
	})
}
func saveToDb(title, imgUrl, year, score, quote, director, actor string) {
	err := engine.Sync(new(MovieData))
	if err != nil {
		fmt.Println("保存数据出现错误")
	}
	moviedata := MovieData{
		Title:    title,
		Year:     year,
		Score:    score,
		Director: director,
		Actor:    actor,
		Quote:    quote,
		Picture:  imgUrl,
	}
	affected, err := engine.Insert(&moviedata)
	if err != nil {
		fmt.Println("插入出现错误!")
	}
	fmt.Println("保存:", strconv.FormatInt(affected, 10))
}
func InfoHandler(info string) (year, director, actor string) {
	year_re, _ := regexp.Compile(`(\d+)`)
	year = string(year_re.Find([]byte(info)))
	director_re, _ := regexp.Compile(`导演:(.*?)\s*(主演:|$)`)
	director_result := director_re.FindStringSubmatch(info)
	if len(director_result) > 1 {
		director = director_result[1]
	}
	actor_re, _ := regexp.Compile(`主演:(.*)`)
	actor_result := actor_re.FindStringSubmatch(info)
	if len(actor_result) > 1 {
		actor = actor_result[1]
	}
	return year, director, actor
}

爬B站评论
Go语言爬虫还是挺累人,它在解析json数据要使用结构体的形式,这里可以使用 json2struct.mervine.net网站来转换成结构体,并提取出评论部分。
package main
import (
	"fmt"
	"github.com/goccy/go-json"
	"io"
	"log"
	"net/http"
)
type KingRankResp struct {
	Code int64 `json:"code"`
	Data struct {
		Replies []struct {
			Content struct {
				Device  string        `json:"device"`
				JumpURL struct{}      `json:"jump_url"`
				MaxLine int64         `json:"max_line"`
				Members []interface{} `json:"members"`
				Message string        `json:"message"`
				Plat    int64         `json:"plat"`
			} `json:"content"`
			Count  int64 `json:"count"`
			Folder struct {
				HasFolded bool   `json:"has_folded"`
				IsFolded  bool   `json:"is_folded"`
				Rule      string `json:"rule"`
			} `json:"folder"`
			Like    int64 `json:"like"`
			Replies []struct {
				Action  int64 `json:"action"`
				Assist  int64 `json:"assist"`
				Attr    int64 `json:"attr"`
				Content struct {
					Device  string   `json:"device"`
					JumpURL struct{} `json:"jump_url"`
					MaxLine int64    `json:"max_line"`
					Message string   `json:"message"`
					Plat    int64    `json:"plat"`
				} `json:"content"`
				Rcount  int64       `json:"rcount"`
				Replies interface{} `json:"replies"`
			} `json:"replies"`
			Type int64 `json:"type"`
		} `json:"replies"`
	} `json:"data"`
	Message string `json:"message"`
}
func main() {
	client := &http.Client{}
	req, err := http.NewRequest("GET", "https://api.bilibili.com/x/v2/reply/wbi/main?oid=338159898&type=1&mode=3&pagination_str={\"offset\":\"\"}&plat=1&seek_rpid=&web_location=1315875&w_rid=1b7a0abc9704055facaafb28b36d864e&wts=1704203337", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("authority", "api.bilibili.com")
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36")
	req.Header.Set("accept", "*/*")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	var resultList KingRankResp
	_ = json.Unmarshal(bodyText, &resultList)
	for _, result := range resultList.Data.Replies {
		fmt.Println("一级评论:", result.Content.Message)
		for _, reply := range result.Replies {
			fmt.Println("二级评论:", reply.Content.Message)
		}
	}
}

















![[足式机器人]Part2 Dr. CAN学习笔记-自动控制原理Ch1-8Lag Compensator滞后补偿器](https://img-blog.csdnimg.cn/direct/aa601dd806934390a440e1038e13d16e.png#pic_center)
