Go语言基础简单了解

44 分钟

前言

简单的入门一下Go、会对基础语法、网络编程、Gin开发进行简单的了解,关键是Gin开发。

关于Go

Go语言(也称为Golang)是Google开发的一种开源编程语言。它被设计用于构建高效、可靠和可扩展的软件系统。下面是Go语言的一些主要用途:

  1. 服务器端开发:Go语言提供了强大的标准库和并发模型,使其成为构建高性能网络服务器的理想选择。许多大型互联网公司正在使用Go语言来开发后端服务,以处理高负载和并发请求。
  2. 网络编程:Go语言提供了丰富的网络编程库,可用于开发各种网络应用程序,包括Web服务、API服务器、网络代理等。
  3. 分布式系统:Go语言的并发模型和原生支持的并发原语(goroutine和channel)使其非常适合构建分布式系统,例如数据处理管道、消息队列等。
  4. 命令行工具:Go语言的编译速度快,生成的可执行文件体积小,使其成为开发命令行工具的良好选择。许多开发者使用Go语言来构建工具、脚本和自动化任务。
  5. 嵌入式系统:Go语言可以用于编写嵌入式系统的控制逻辑和驱动程序。它提供了对底层硬件的访问和控制能力,并具有较小的内存消耗。

需要注意的是,Go语言具有简洁而直观的语法,易于学习和使用。它的性能非常好,可以充分利用多核处理器和并发编程来提高应用程序的性能和吞吐量。

学习流程

基础语法->Web开发->常见中间件->云平台

基础语法

注释

增强语言的可读性

  1. 单行注释

  2. 多行注释

    package main
    
    import "fmt"
    
    // 单行注释
    /*
    多行注释
    多行注释
    */
    func main() {
    fmt.Println("hello world")
    }
    

变量

  1. var 定义变量,var 变量名 变量类型。
  2. 简短变量声明,使用:=运算符可以在函数内部声明并初始化变量。
  3. 匿名变量,使用 _ 占位符可以声明一个匿名变量,忽略不需要的值,任何赋值給这个标识符的值都将被抛弃,并且不会导致变量的冲突
  4. 定义多个变量,可以使用()包裹,表示定义多个变量
  5. 注意点:变量名的首个字符不能为数字,全局变量可被局部变量再定义(就近原则 ),定义的变量一定要使用。

变量声明后的默认值:

  • 整数型浮点数变量默认值是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)
}

常量

  1. 使用const来定义常量,不可改变
  2. 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

}

数据类型

  1. 布尔型bool,默认值是false

  2. 数字类型,分为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)
  3. float浮点型,默认是64位,保留6位小数,保留小数会丢失精度,采取四舍五入的原则

    序号 类型和描述
    1 float32 IEEE-754 32位浮点型数
    2 float64 IEEE-754 64位浮点型数
    3 complex64 32 位实数和虚数
    4 complex128 64 位实数和虚数
  4. 类型别名,Go语言会有一些类型的别名

    序号 类型和描述
    1 byte类似uint8
    2 int和uint一样大小
    3 rune类似int32
    7 uintptr无符号整型,用于存放指针
  5. 字符类型,' 和" 和双引号包裹的字符是有差别的,' 默认是int32 类型,会自动转换成Unicode 编码的值,字符可以直接使用+ 连接

  6. 类型转换 ,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库

常用的一些函数

  1. Print / Println / Printf:格式化并输出到标准输出。
  2. Println:类似于 Print,但在输出后添加换行符。
  3. Printf:使用格式化字符串进行输出(类似于 C 语言中的 printf 函数)。
  4. Sprint / Sprintln / Sprintf:将格式化的结果以字符串形式返回,而不是输出到标准输出。
  5. Fprint / Fprintln / Fprintf:将格式化的结果输出到指定的文件(io.Writer)。
  6. Errorf:生成一个格式化的错误字符串。
  7. Scan / Scanln / Scanf:从标准输入读取并格式化输入。
  8. Sscan / Sscanln / Sscanf:从给定的字符串中读取并格式化输入。
  9. 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)
    }
}

函数

  1. 函数是一个基本代码块,用于执行一个任务
  2. Go语言最少有一个main函数
  3. 函数声明告诉编译器函数的名称,返回类型,参数
  4. 函数本身也是一个变量,也可以进行赋值
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

  1. init()函数不能被其它函数调用,而是在main函数执行之前自动被调用

  2. init 函数不能作为参数传入,不能有传入参数和返回值

  3. 当一个main有多个init函数,谁在前谁就先执行

匿名、回调、闭包函数

  1. 也叫闭包函数(closures),允许临时创建一个没有指定名称的函数
  2. 回调函数就是一个函数作为另一个函数的参数。
  3. 闭包结构,一个外层函数中有内层函数,该内层函数中可以操作外层函数的局部变量,并且外层函数的返回值是内层函数,这种结构是闭包结构。
  4. 正常的局部变量会随着函数的调用而创建,随着函数的结束而销毁,但是在闭包结构中的外层函数的局部变量并不会随着外层函数的结果而销毁,因为内层函数还在堆栈中使用
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)

  1. 不能绑定方法
  2. 打印类型还是原始类型
  3. 类型别名不用转换
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)

}

文件读取

  1. os.ReadFile()一次性读取整个文件的内容。

  2. os.Open()分片读取。

  3. 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())
    }
    }
    

文件写入

  1. 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())
    }
    }

    反射

    1. reflect.typeof 获取类型
    2. reflect.Valueof 获取值
    3. 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页面中的节点。

常用元素:

  1. Find: 通过CSS选择器查找元素,例如 doc.Find("div.content") 将返回所有class为content的div元素。
  2. Each: 遍历匹配的元素集合,并对每个元素执行指定的函数。
  3. Text: 获取元素的文本内容。
  4. Attr: 获取元素的属性值。
  5. Html: 获取匹配元素的HTML内容。
  6. Parent, Children, Next, Prev: 获取父元素、子元素、相邻的后一个元素、相邻的前一个元素等。
  7. Filter, Not, HasClass, Is: 根据特定条件对元素集合进行过滤和判断。
  8. AddClass, RemoveClass, ToggleClass: 添加、移除、切换元素的类名。
  9. Serialize: 将匹配的表单元素序列化为URL编码的字符串。
  10. 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 框架,具有以下特点:

  1. 简单易用:Colly 提供了一个简洁、直观的 API,易于使用和理解。
  2. 高度灵活:Colly 允许你自定义请求头、回调函数、处理方法等,以满足各种爬取需求。
  3. 并发支持:Colly 提供了并发请求的支持,可以同时处理多个请求,提高爬取效率。
  4. 支持动态网页:Colly 集成了 PhantomJS,可以处理 JavaScript 渲染的动态网页。
  5. 内置的选择器引擎:Colly 使用自己的选择器引擎,类似于 CSS 选择器,可以轻松地从 HTML 中提取所需的数据。
  6. 自动处理重试和错误:Colly 可以自动处理请求的重试和错误,提供了一种简化错误处理的方法。
  7. 支持代理:Colly 允许你使用代理服务器进行请求,以帮助隐藏真实 IP 地址和绕过访问限制。
  8. 自定义数据存储:Colly 提供了灵活的机制,允许你自定义数据的存储方式,比如输出到文件、存储到数据库等。
  9. 事件驱动:Colly 采用事件驱动的方式,通过注册回调函数处理请求和提取数据。
  10. 广泛的社区支持: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)
        }
    }
}

Gin

Gin框架与flask十分的相似,都是路由、处理函数对应,感觉都十分的轻量化。

响应

启动方式

Gin框架的启动方式库有分为两种:

  1. 使用Run()函数启动,实际上也是第二种的一个封装
  2. 使用原生的http.ListenAndServe()启动

响应JSON、XML、yaml、html

这里对于响应的文件格式方式,都是几乎一致的,只需要使用context.JSON()、contenxt.XML()等换一个函数的形式返回即可。至于返回的数据,一般又三种方式:

  1. 使用struct包装json等形式返回,注意字段开头要大写才能进行json序列化解析
  2. 使用map包装json等形式返回
  3. 使用gin.H{}直接返回数据

至于HTML,只需要通过LoadHTMLGlob()函数指定存放的HTML文件的目录,随后通过context.HTML()指定返回的文件和数据即可在前端完成渲染。

响应静态文件

响应静态文件一般使用以下方式:

对应文件的路径,没有相对路径,只有基于项目的路径,并且两种静态文件的URL前缀是不可重复的。

  1. StaticFile()
    • StaticFile() 方法用于指定单个文件的静态文件服务。
    • 语法:router.StaticFile("/path", "filepath")
  2. StaticFS()
    • StaticFS() 方法用于指定整个文件系统的静态文件服务。
    • 语法:router.StaticFS("/path", http.Dir("rootpath"))

重定向

重定向也是直接使用contenxt.Redirect()函数重定向来地址即可,但是要注意301和302重定向的不同:

  1. 301表示永久重定向,强缓存,会将响应的结果缓存起来
  2. 302是临时重定向,是不会进行缓存的。

查询参数

静态参数

  1. query()获取URL中的参数,如果没有返回空
  2. GetQuery()获取URL的参数key的值,没有返回false
  3. QueryArray()可以拿到多个相同的查询参数

动态参数

动态参数也就是可变的参数,比如说/example/1中的1可通过改变显示不同的查询数据,可通过/:key的形式定义查询的动态参数路径。

表单参数

  1. PostFormArray()可获取多个表单参数,返回数组的形式
  2. PostForm()获取表单参数,获取不到返回空,可以接受form-data和x-www-form-urlencoded两种形式
  3. DefaultForm()假如没有接受到这个参数,可设置默认的值
  4. MultipartForm()接受所有的post参数,包括文件等,返回Map的形式。

原始参数

GetRawData()可以获取原始参数,如果是x-www-form-urlencoded返回key=value的ASCII码值,如果是form-data返回Content-Dispostion的那种形式。

image-20240108215044824

demo:

package main

import (
    "encoding/json"
    "fmt"
    "github.com/gin-gonic/gin"
    "net/http"
)

func Index(context *gin.Context) {
    context.String(200, "Hello World")
}

func Json(context *gin.Context) {
    //开头字母一定要大写才能使Json序列化可见
    type User struct {
        Username string
        Nickname string
        Password string `json:"-"` //json不进行响应
    }
    userInfo := User{Username: "Aiwin", Nickname: "Lau", Password: "123456"}
    context.JSON(200, userInfo)

    //map响应
    /*userMap := map[string]string{
        "username":  "Aiwin",
        "nickename": "Lau",
    }
    context.JSON(200, userMap)*/

    //直接响应json
    //context.JSON(200, gin.H{"username": "aiwin", "nickename": "Lau"})

}

// Html 响应HTML页面
func Html(context *gin.Context) {
    context.HTML(200, "index.html", gin.H{"username": "Aiwin"})
}
func redirect(context *gin.Context) {
    //永久重定向,强缓存,将响应结果缓存起来
    context.Redirect(301, "http://www.aiwin.net.cn")
    //临时重定向,不会进行缓存
    context.Redirect(302, "http://www.aiwin.net.cn")
}

// 获取静态查询参数
func query(context *gin.Context) {
    fmt.Println(context.GetQuery("user"))
    fmt.Println(context.GetQueryArray("user"))
}

// 获取动态路径参数
func param(context *gin.Context) {
    fmt.Println(context.Param("user_id"))

}

// form表单
func form(context *gin.Context) {
    fmt.Println(context.PostForm("name"))
    fmt.Println(context.PostFormArray("name"))
    fmt.Println(context.DefaultPostForm("age", "18"))
}

func raw(context *gin.Context) {
    body, _ := context.GetRawData()
    contentType := context.GetHeader("Content-Type")
    //解析json数据
    switch contentType {
    case "application/json":
        type User struct {
            Username string `json:"username"`
            Password string `json:"password"`
        }
        var user User
        err := json.Unmarshal(body, &user)
        if err != nil {
            fmt.Println(err)

        }
        fmt.Println(user)

    }
}

func main() {

    Engine := gin.Default()
    Engine.LoadHTMLGlob("网络编程/gin/templates/*")
    //GoLang中只有相对项目路径,响应下载文件
    Engine.StaticFile("/static/demo", "网络编程/gin/static/demo.jpg")
    //注意前缀不要重复,否则会报错
    Engine.StaticFS("/fsfile", http.Dir("网络编程/gin/static"))

    //定于路由和对应的处理函数
    Engine.GET("/index", Index)
    Engine.GET("/json", Json)
    Engine.GET("/html", Html)
    Engine.GET("/redirect", redirect)
    Engine.GET("/query", query)
    Engine.GET("/param/:user_id", param)
    Engine.POST("/form", form)
    Engine.POST("/raw", raw)
    //第一种启动方式
    Engine.Run("127.0.0.1:8000")
    //第二种启动方式,用原生的http服务器,Engine.Run只是原生http的封装
    //http.ListenAndServe("127.0.0.1:5000", Engine)

}

四大请求方式

RESTful 风格是一种用于设计和构建网络应用程序的软件架构风格。它基于 HTTP 协议,并遵循一些设计原则和约束,旨在提供简单、可扩展、可靠和可轻松集成的 Web 服务。

  1. GET:从服务器取除资源
  2. POST在服务器中新建一个资源
  3. PUT在服务器中更新资源
  4. PATCH在服务器中更新资源
  5. DELETE从服务器中删除资源

简单demo演示:

GET  文章列表
GET /article/:id 文章详情
POST 添加文章
PUT /article/:id 更新文章
DELETE 删除文章
package main

import (
    "encoding/json"
    "fmt"
    "github.com/gin-gonic/gin"
    "strconv"
)

type article struct {
    Title  string `json:"title"`
    Detail string `json:"detail"`
}
//模拟数据库
var articlelist = []article{
    {"西游记", "《九九八十一难》"},
    {"水浒传", "《梁山一百零八好汉》"},
    {"三国演义", "《三国鼎立传奇故事》"},
}

type response struct {
    Code int    `json:"code"`
    Data any    `json:"data"`
    Msg  string `json:"msg"`
}

func _bindJson(context *gin.Context, obj any) (err error) {
    body, _ := context.GetRawData()
    contentType := context.GetHeader("Content-Type")
    switch contentType {
    case "application/json":
        err = json.Unmarshal(body, &obj)
        if err != nil {
            fmt.Println(err.Error())
            return err
        }

    }
    return nil
}
func getlist(context *gin.Context) {
    context.JSON(200, response{Code: 0, Data: articlelist, Msg: "成功"})

}
func getDetail(context *gin.Context) {
    id, _ := strconv.Atoi(context.Param("id"))
    context.JSON(200, response{Code: 0, Data: articlelist[id], Msg: "成功"})
}
func create(context *gin.Context) {
    var newarticle article
    err := _bindJson(context, &newarticle)
    if err != nil {
        fmt.Println(err)
    }
    articlelist = append(articlelist, newarticle)
    context.JSON(200, response{Code: 0, Data: nil, Msg: "添加成功"})
}

func edit(context *gin.Context) {
    id, _ := strconv.Atoi(context.Param("id"))
    var editarticle article
    err := _bindJson(context, &editarticle)
    if err != nil {
        fmt.Println(err)
    }
    articlelist[id].Title = editarticle.Title
    articlelist[id].Detail = editarticle.Detail
    context.JSON(200, response{Code: 0, Data: editarticle, Msg: "修改成功"})
}

func delete(context *gin.Context) {
    id, _ := strconv.Atoi(context.Param("id"))
    articlelist = append(articlelist[:id], articlelist[id+1:]...)
    context.JSON(200, response{Code: 0, Data: articlelist, Msg: "删除成功"})
}

func main() {
    router := gin.Default()
    router.GET("/getlist", getlist)
    router.GET("/getDetail/:id", getDetail)
    router.POST("/create", create)
    router.PUT("/edit/:id", edit)
    router.DELETE("/delete/:id", delete)
    router.Run("0.0.0.0:8888")
}

请求头相关

通过getHeader()获取请求头,不区分大小写,使用-来进行连接,通过Request.Header可获取全部请求头的map,但是要注意Request.Header如果用Map取值方式是必须大小写一致的(因为是从Map 中获取value的)。

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

func index(context *gin.Context) {
    fmt.Println(context.GetHeader("user-agent"))
    fmt.Println(context.Request.Header.Get("user-agent"))
    fmt.Println(context.Request.Header["User-Agent"])
    context.Header("token", "Hello-World")
    context.Header("Content-Type", "application/text;charset=utf-8")
    context.JSON(0, gin.H{"data": "自定义响应头"})
}
func main() {
    router := gin.Default()
    router.GET("/index", index)
    router.Run("0.0.0.0:8080")
}

绑定参数

gin中可以使用bind快速将前端传递来的数据与结构体进行参数绑定,以及参数校验。

  1. ShouldBind:在绑定过程中如果出现错误,将错误信息返回给调用者,由调用者决定如何处理错误。
  2. MustBind:在绑定过程中如果出现错误,将直接触发panic,使程序进入恐慌状态并终止运行。
package main

import "github.com/gin-gonic/gin"

// form指示可以接受form表单
// uri表示可以通过uri query查询
type User struct {
    Name string `json:"name" form:"name" uri:"name"`
    Age  int    `json:"age" form:"age" uri:"age"`
    Sex  string `json:"sex" form:"sex" uri:"sex"`
}

func json(context *gin.Context) {
    var user User
    err := context.ShouldBindJSON(&user)
    if err != nil {
        context.JSON(200, gin.H{"msg": "出错了"})
        return
    }
    context.JSON(200, user)
}

func uri(context *gin.Context) {
    var user User
    err := context.ShouldBindUri(&user)
    if err != nil {
        context.JSON(200, gin.H{"msg": "出错了"})
        return
    }
    context.JSON(200, user)
}
func form(context *gin.Context) {
    var user User
    err := context.ShouldBind(&user)
    if err != nil {
        context.JSON(200, gin.H{"msg": "出错了"})
        return
    }
    context.JSON(200, user)
}

func main() {
    router := gin.Default()
    router.GET("/json", json)
    router.POST("/form", form)
    router.GET("/uri/:name/:age/:sex", uri)
    router.Run("0.0.0.0:8888")
}

常用验证器

  1. required:字段必需存在且不能为空。例如,required:"true"。
  2. min:字段的值必须大于或等于指定的最小值。可以用于数字,字符串和时间类型的验证。例如,min:"10"。
  3. max:字段的值必须小于或等于指定的最大值。同样适用于数字,字符串和时间类型的验证。例如,max:"100"。
  4. len:字段的长度必须等于指定的值。例如,len:"10"。
  5. email:字段的值必须为有效的电子邮件地址。例如,email:"true"。
  6. url:字段的值必须为有效的URL地址。例如,url:"true"。
  7. regex:字段的值必须符合指定的正则表达式。例如,regex:"^[A-Za-z]+$"。
  8. alpha:字段的值必须全部由字母组成,不允许包含空格、数字或特殊字符。例如,alpha:"true"。
  9. alphanum:字段的值必须全部由字母和数字组成,不允许包含空格或特殊字符。例如,alphanum:"true"。
  10. numeric:字段的值必须为数字型的数据,可以是整数或浮点数。例如,numeric:"true"。
  11. oneof:字段的值必须属于提供的多个值中的一个。例如,oneof:"dog cat bird"。
  12. gte:字段的值必须大于或等于指定的另一个字段的值。例如,gte:"Age"。
  13. lte:字段的值必顶小于或等于指定的另一个字段的值。例如,lte:"EndTime"。
  14. eqfield,nefield等于和不等于其它字段的值
  15. contains 一定要包含的字符串
  16. excludes 不包含的字符串
  17. startswith 字符串前缀
  18. endwith 字符串后缀
  19. ip ipv4 ipv6 uri url 特定的格式
  20. datetime 日期验证,如datetime=2002-03-04 15:00:01等表示日期验证
  21. dive 针对数组
package main

import "github.com/gin-gonic/gin"

type Member struct {
    Username    string `json:"username" binding:"required,min=5,max=10"`
    Password    string `json:"password" binding:"required" `
    Re_password string `json:"re_password" binding:"eqfield=Password"`
}

func verifyJson(context *gin.Context) {
    var member Member
    err := context.ShouldBindJSON(&member)
    if err != nil {
        context.JSON(200, gin.H{"msg": err.Error()})
        return
    }
    context.JSON(200, member)
}

func main() {
    router := gin.Default()
    router.GET("/verifyJson", verifyJson)
    router.Run("0.0.0.0:8888")
}

自定义错误信息

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
    "reflect"
)

type MemberInfo struct {
    Username    string `json:"username" binding:"required,min=5,max=10" msg:"用户名长度不符合"`
    Password    string `json:"password" binding:"required" msg:"密码不能为空"`
    Re_password string `json:"re_password" binding:"eqfield=Password" msg:"两次密码必须一致"`
}

func GetValidMsg(err error, obj any) string {
    //obj是结构体指针
    getObj := reflect.TypeOf(obj)
    //将err接口断言为是否为验证器返回的错误
    if errs, ok := err.(validator.ValidationErrors); ok {
        //循环每一个错误信息
        for _, e := range errs {
            //根据报错字段拿到自定义错误信息
            if f, exist := getObj.Elem().FieldByName(e.Field()); exist {
                return f.Tag.Get("msg")
            }
        }
    }
    return err.Error()
}
func Mymessage(context *gin.Context) {
    var member MemberInfo
    err := context.ShouldBindJSON(&member)
    if err != nil {
        context.JSON(200, gin.H{"msg": GetValidMsg(err, &member)})
        return
    }
    context.JSON(200, member)
}

func main() {
    router := gin.Default()
    router.GET("/verifyJson", Mymessage)
    router.Run("0.0.0.0:8888")
}

自定义验证器

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
    "reflect"
)

type Student struct {
    Name string `json:"name" binding:"required,sign" msg:"用户名校验失败"`
    Age  int    `json:"age" binding:"required"`
}

func _GetValidMsg(err error, obj any) string {
    getObj := reflect.TypeOf(obj)
    if errs, ok := err.(validator.ValidationErrors); ok {
        for _, e := range errs {
            if f, exist := getObj.Elem().FieldByName(e.Field()); exist {
                return f.Tag.Get("msg")
            }
        }
    }
    return err.Error()
}

func verify(context *gin.Context) {
    var student Student
    err := context.ShouldBindJSON(&student)
    if err != nil {
        context.JSON(200, gin.H{"msg": _GetValidMsg(err, &student)})
        return
    }
    context.JSON(200, student)
}
func signValid(fl validator.FieldLevel) bool {
    var namelist = []string{"admin"}
    for _, namestr := range namelist {
        name := fl.Field().Interface().(string)
        if name == namestr {
            return false
        }
    }
    return true

}
func main() {
    Engine := gin.Default()
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok { //自定义验证器
        v.RegisterValidation("sign", signValid)
    }
    Engine.POST("/verify", verify)

    Engine.Run("0.0.0.0:8000")

}

文件上传

  1. saveUploadFile()直接保存文件到服务器 ,本质上还是Create+Copy

  2. Ceate+Copy 可以直接通过Create创建文件,通过Copy保存Open()出来的内容

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "io"
    "os"
)

func upload(context *gin.Context) {
    files, err := context.FormFile("file")
    if err != nil {
        context.String(500, "上传失败")
        return
    }
    fileInterface, _ := files.Open()
    //通过SaveUploadedFile()进行处理, 本质上还是Create+Copy
    //err = context.SaveUploadedFile(files, "网络编程/文件上传/uploads/"+files.Filename)
    //if err != nil {
    //  context.JSON(500, "上传失败")
    //  return
    //}
    fmt.Println(files.Filename)
    result, _ := os.Create("网络编程/文件上传/uploads/" + files.Filename)
    defer result.Close()
    io.Copy(result, fileInterface)
    context.String(200, "上传成功")

}

func uploads(context *gin.Context) {
    form, err := context.MultipartForm()
    if err != nil {
        context.String(500, "上传错误")
        return
    }
    files := form.File["file[]"]
    for _, file := range files {
        context.SaveUploadedFile(file, "网络编程/文件上传/uploads/"+file.Filename)
    }
    context.String(200, "上传成功")
}
func main() {
    Engine := gin.Default()
    Engine.POST("/upload", upload)
    Engine.POST("/uploads", uploads)
    Engine.Run("0.0.0.0:8000")
}

文件下载

通过Header()设置文件的方式,唤起浏览器进行下载。

func download(context *gin.Context) {
    context.Header("Content-Type", "application/octet-stream")
    context.Header("Content-Disposition", "attachment; filename=download.jpg")
    context.File("网络编程/文件上传/uploads/5.jpg")
}

中间件

Gin框架允许开发者在处理请求的过程中,假如自己的函数也就是中间件,一般在函数处理的前面,可用于登录认证,权限校验,数据分页等等。

在定义路由的时候,其实可以定义多个函数按顺序进行处理,路由对应的处理函数其实也是一个中间件,但是一般处理路由不会进行验证拦截,而我们可以在真正的处理函数前面定义其它函数并进行一些Abort()验证拦截操作,当然也可以通过Next()指定下一个要走的中间件,如果后面没有中间件,则会走回去第一个中间件。一般用的比较多的是全局中间件。

这里记录下gin.Default()和gin.New()的配置:

  • gin.Default() 方法会返回一个默认的 Gin 引擎实例,并且会自动注入 Logger 和 Recovery 中间件,同时也会使用默认的配置,包括日志输出到控制台等。
  • gin.New() 方法则会返回一个裸露的 Gin 引擎实例,不会自动注入任何中间件或默认配置,需要手动进行配置。
package main

import "github.com/gin-gonic/gin"

func middleware(context *gin.Context) {
    username, ok := context.GetQuery("username")
    if ok {
        if username == "admin" {
            context.Set("name", "admin") //中间件传递值
            context.Next()
        } else {
            context.JSON(200, gin.H{"msg": "你不是admin"})
            context.Abort()
        }
    }
}
func main() {
    router := gin.Default()
    router.Use(middleware)
    router.GET("/login", func(context *gin.Context) {
        name, _ := context.Get("name")
        context.JSON(200, gin.H{"msg": "欢迎" + name.(string) + "用户"})

    })
    router.Run("0.0.0.0:8000")
}

路由分组

就是给路由匹配一些前缀路由,避免重复书写,比较方便,也比较美观。

package main

import "github.com/gin-gonic/gin"

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
type Article struct {
    Title   string `json:"title"`
    Content string `json:"content"`
}
type Response struct {
    Code int    `json:"code"`
    Data any    `json:"data"`
    Msg  string `json:"msg"`
}

func Usermiddleware(ctx *gin.Context) {
    token := ctx.GetHeader("token")
    if token == "admin" {
        ctx.Next()
    } else {
        ctx.JSON(403, gin.H{"msg": "未通过验证"})
        ctx.Abort()
    }
}
func UserListView(ctx *gin.Context) {
    var userList = []User{
        {"Aiwin", 20},
        {"zhangsan", 21},
        {"lisi", 22},
    }
    ctx.JSON(200, Response{0, userList, "请求成功"})
}
func getArticleList(ctx *gin.Context) {
    var articleList = []Article{
        {"《变形记》", "讲变形的故事"},
        {"《西游记》", "讲取经的故事"},
        {"《小王子》", "讲王子的故事"},
    }
    ctx.JSON(200, Response{0, articleList, "请求成功"})
}

func main() {
    router := gin.Default()
    api := router.Group("api")                                //定义路由分组
    api.GET("/getUserList", UserListView).Use(Usermiddleware) // /api/getUserList
    article := api.Group("article")                           //双重路由
    article.GET("/getArticleList", getArticleList)            ///api/article/getUserList
    router.Run("0.0.0.0:8000")
}

日志

  1. 记录用户操作,猜测用户行为
  2. 记录bug

通过gin.DefaultWriter()来指定一个os.Create()的file,能够利用内置的日志器。

gin.DebugPrintRouteFunc()用于打印当前注册的所有路由信息,可以使用Route()函数打印所有的路由更快。

如果不想看debug信息,可以通过gin.SetMode()切换环境

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "io"
    "os"
)

func LogFormatterParams(params gin.LogFormatterParams) string {
    return fmt.Sprintf(
        "[Aiwin] %s | %s%d%s | %s %s %s    %s \n",
        params.TimeStamp.Format("2024-01-16 15:00:00"),
        params.StatusCodeColor(), params.StatusCode, params.ResetColor(),
        params.MethodColor(), params.Method, params.ResetColor(),
        params.Path)
}
func main() {
    gin.SetMode(gin.ReleaseMode)
    file, _ := os.Create("log.txt")
    gin.DefaultWriter = io.MultiWriter(file, os.Stdout)
    router := gin.Default()

    //自定义打印日志的格式
    //router := gin.New()
    //router.Use(gin.LoggerWithFormatter(LogFormatterParams);
    //router.Use(gin.LoggerWithConfig(gin.LoggerConfig{Formatter: LogFormatterParams}))
    for _, tree := range router.Routes() {
        fmt.Println(tree.Method, tree.Path, tree.HandlerFunc)
    }
    router.GET("/index", func(context *gin.Context) {})
    router.Run("0.0.0.0:8000")
}

logrus

go get github.com/sirupsen/logrus

一些常用的命令,少于Info等级就不会打印出来,可以通过logrus.SetLevel()更改等级:

  1. logrus.Error()错误信息

  2. logrus.Warnln()警告信息

  3. logrus.Infof()普通信息

  4. logrus.Debug()调试信息

  5. logrus.Println()打印信息

  6. logrus.SetFormatter()一些字段配置

    名称 作用
    ForceColors 是否强制使用颜色输出
    DisableColors 是否禁用颜色输出
    ForceQuote 是否强制引用所有值
    DisableQuote 是否禁止引用所有值
    DisableTimestamp 是否禁用时间戳记录
    FullTimestamp 是否连接到TTY时输出完整的时间戳
    TimestampFormat 用于输出完整时间戳的时间戳格式
  7. 一些颜色的输出如下:

     fmt.Println("\033[30m 黑色 \033[0m")
     fmt.Println("\033[31m 红色 \033[0m")
     fmt.Println("\033[32m 绿色 \033[0m")
     fmt.Println("\033[33m 黄色 \033[0m")
     fmt.Println("\033[34m 蓝色 \033[0m")
     fmt.Println("\033[35m 紫色 \033[0m")
     fmt.Println("\033[36m 青色 \033[0m")
     fmt.Println("\033[37m 灰色 \033[0m")
     // 背景色
     fmt.Println("\033[40m 黑色 \033[0m")
     fmt.Println("\033[41m 红色 \033[0m")
     fmt.Println("\033[42m 绿色 \033[0m")
     fmt.Println("\033[43m 黄色 \033[0m")
     fmt.Println("\033[44m 蓝色 \033[0m")
     fmt.Println("\033[45m 紫色 \033[0m")
     fmt.Println("\033[46m 青色 \033[0m")
     fmt.Println("\033[47m 灰色 \033[0m")

    自定义logrus输出格式:

    package main
    
    import (
    "bytes"
    "fmt"
    "github.com/sirupsen/logrus"
    "os"
    "path"
    )
    
    const (
    red    = 31
    yellow = 33
    blue   = 36
    gray   = 37
    )
    
    type LogFormatter struct{}
    
    func (t *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
    var levelColor int
    switch entry.Level {
    case logrus.DebugLevel, logrus.TraceLevel:
        levelColor = gray
    case logrus.WarnLevel:
        levelColor = yellow
    case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
        levelColor = red
    default:
        levelColor = blue
    
    }
    var b *bytes.Buffer
    if entry.Buffer != nil {
        b = entry.Buffer
    } else {
        b = &bytes.Buffer{}
    }
    timestamp := entry.Time.Format("2006-01-02 15:04:05")
    if entry.HasCaller() {
        //自定义文件路径
        funcVal := entry.Caller.Function
        fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line)
        //自定义输出格式
        fmt.Fprintf(b, "[%s] \x1b[%dm[%s]\x1b[0m %s %s %s\n", timestamp, levelColor, entry.Level, fileVal, funcVal, entry.Message)
    } else {
        fmt.Fprintf(b, "[%s] \x1b[%dm[%s]\x1b[0m %s\n", timestamp, levelColor, entry.Level, entry.Message)
    }
    return b.Bytes(), nil
    
    }
    
    var log *logrus.Logger
    
    func init() {
    log = NewLog()
    }
    func NewLog() *logrus.Logger {
    mLog := logrus.New()               //新建一个实例
    mLog.SetOutput(os.Stdout)          //设置输出类型
    mLog.SetReportCaller(true)         //开启返回函数名和行号
    mLog.SetFormatter(&LogFormatter{}) //设置自己定义的Formatter
    mLog.SetLevel(logrus.DebugLevel)   //设置最低的Level
    return mLog
    }
    func main() {
    log.Errorln("你好")
    log.Infof("你好")
    log.Warnln("你好")
    log.Debugf("你好")
    }
    
~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:开发,开发
文章标题:Go语言基础简单了解
文章链接:https://aiwin.net.cn/index.php/archives/3907/
最后编辑:2025 年 5 月 6 日 13:09 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 4 + 6 =
快来做第一个评论的人吧~