Golang+Gin框架进行HTTP开发,记录一些学习笔记

Viper 

Viper是一个强大的配置管理库,主要用于管理Go应用的配置信息。它可以处理多种配置来源,包括配置文件、环境变量、命令行参数等

Githu仓库:https://github.com/spf13/viper

1.读取配置文件

假设有一个config.yaml配置文件,使用Viper读取配置文件的代码如下:

package main

import (
    "fmt"
    "log"
    "github.com/spf13/viper"
)

func main() {
    // 设置配置文件路径和名称
    viper.SetConfigName("config") // 配置文件名(不带扩展名)
    viper.SetConfigType("yaml")   // 配置文件类型
    viper.AddConfigPath(".")      // 配置文件所在路径

    // 读取配置文件
    if err := viper.ReadInConfig(); err != nil {
        log.Fatalf("Error reading config file, %s", err)
    }

    // 获取配置值
    appName := viper.GetString("app_name")
    debug := viper.GetBool("debug")
    dbHost := viper.GetString("database.host")
    dbPort := viper.GetInt("database.port")
    dbUser := viper.GetString("database.username")
    dbPass := viper.GetString("database.password")
}

2.设置默认值

Viper支持为配置项设置默认值,这在配置项未在配置文件中定义,但我又需要一个默认值的情况下非常有用 :

viper.SetDefault("database.port", 3306)

3.绑定环境变量

Viper可以自动从环境变量中读取配置 :

viper.AutomaticEnv()
viper.SetEnvPrefix("myapp")

4.动态监听配置变化

Viper可以监听配置文件的变化,并在文件更新时自动重新加载配置:

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
    fmt.Println("Config file changed:", e.Name)
})

5.绑定到结构体

Viper可以将配置解析到结构体中:

type AppConfig struct {
    AppName  string `mapstructure:"app_name"`
    Debug    bool   `mapstructure:"debug"`
    Database struct {
        Host     string `mapstructure:"host"`
        Port     int    `mapstructure:"port"`
        Username string `mapstructure:"username"`
        Password string `mapstructure:"password"`
    } `mapstructure:"database"`
}

var config AppConfig
if err := viper.Unmarshal(&config); err != nil {
    panic(err)
}

6.读取多个配置文件

Viper可以通过多次调用viper.ReadInConfig()来逐个读取多个配置文件。Viper会按照读取的顺序覆盖配置项,后面的配置文件中的相同键会覆盖前面的配置文件中的值。

Viper可以调用viper.MergeConfig方法可以将多个配置文件合并到一起。

package main

import (
    "fmt"
    "log"
    "github.com/spf13/viper"
)

func main() {
    // 读取第一个配置文件
    viper.SetConfigName("config1") // 配置文件名(不带扩展名)
    viper.SetConfigType("yaml")   // 配置文件类型
    viper.AddConfigPath(".")      // 配置文件所在路径
    if err := viper.ReadInConfig(); err != nil {
        log.Fatalf("Error reading config file, %s", err)
    }

    // 读取第二个配置文件并合并
    viper.SetConfigName("config2") // 配置文件名(不带扩展名)
    if err := viper.MergeInConfig(); err != nil {
        log.Fatalf("Error reading config file, %s", err)
    }
}

也可以省略多次调用viper.SetConfigName,直接合并指定路径的配置文件:

// 读取第二个配置文件并合并
if err := viper.MergeConfigFile("./config2.yaml"); err != nil {
    log.Fatalf("Error reading config file, %s", err)
}
在Go应用中,一旦对Viper进行了初始化,其他模块就可以直接引用而无需重新初始化。这是因为Viper使用了全局变量来存储配置信息,这些配置信息在应用范围内是共享的。

Go模块 

模块(Module)是一个相对独立的代码集合,它有自己独立的依赖关系。

1.概念

Go模块是以go.mod文件为标志的代码集合。一个模块可以包含多个包(Package),而包是Go语言中组织代码的基本单元。当

在一个目录下运行go mod init 命令时,就会在该目录下创建一个go.mod文件,从而将该目录及其子目录下的代码定义为一个模块。通常是模块的导入路径,一般是一个以域名开头的字符串,用来唯一标识这个模块。

每个模块都有自己的go.mod和go.sum文件,用于记录依赖关系和依赖的版本信息。 (类似于JS的package.json

go get命令的作用是下载并安装指定的 Go 包及其依赖项,会自动解析这个Go包的依赖关系,并下载所有必要的依赖包。

2.go.mod

在 Go 语言中,一个项目目录下的子目录并不一定都是单独的包。是否构成一个包,取决于目录中是否包含 Go 源代码文件(.go 文件)以及这些文件的包声明,包声明的相关命令:

  • module:声明模块的名称。
  • go:声明模块兼容的最低 Go 版本。
  • toolchain:指定构建模块时使用的 Go 工具链版本。
  • require:声明模块的依赖关系。

3.go mod vendor

go mod vendor会将依赖包下载到当前项目的vendor目录,编译时执行如下命令将使用vendor内的依赖包:

# 编译(使用本地依赖)
go build -mod=vendor main.go

# 运行(使用本地依赖)
go run -mod=vendor main.go

知识点

struct 是一种数据类型,用于将多个不同类型的数据组合成一个逻辑单元。interface 是一种类型,用于定义一组方法的集合,它只包含方法签名,不包含数据字段。

1.Struct

struct 是一种用户自定义的数据类型,用于将多个不同类型的数据组合成一个逻辑单元。它类似于其他语言中的类,但没有方法,主要用途:

  • 用于表示复杂的数据结构。
  • 可以包含字段(成员变量)。
  • 可以通过方法(绑定到结构体的函数)来操作数据。
struct是基础类型,作为函数参数时,会复制整个struct。map、channel、slice是引用类型,变量值本身保存的就是指针

2.Interface

interface 是一种类型,它定义了一组方法的集合。一个类型只要实现了接口中定义的所有方法,就自动实现了该接口。主要还是用于方便各种方法进行调用

  • 定义行为规范(必须传递包含哪些行为方法的数据、变量可以被赋值任意实现行为的数据)
  • 实现多态,方法被调用时可以传递不同类型变量,只要它实现了interface的方法
  • 解耦合,interface可以解耦具体实现和使用代码,使得代码更加模块化和可维护。

new和make

new 和 make 是两个用于分配内存的内置函数,但它们的用途和行为有很大不同。

  • new:分配内存,但不初始化。它会分配零值。
  • make:分配内存并初始化。它只能用于切片、map 和 channel。

new(T) 会分配一个类型为 T 的零值,并返回指向该值的指针。当需要一个指向某个类型的零值的指针时,使用 new。:

package main

import "fmt"

func main() {
    // 使用 new 分配一个 int 类型的零值
    p := new(int)
    fmt.Println(*p) // 输出 0,因为 int 的零值是 0
    *p = 42
    fmt.Println(*p) // 输出 42
}

make(T, size) 会分配一个类型为 T 的内存,并初始化,返回该内存的引用。T 只能是切片、map 或 channel。当需要初始化一个切片、map 或 channel 时,使用 make:

package main

import "fmt"

func main() {
    // 使用 make 初始化一个切片
    s := make([]int, 5) // 创建一个长度为 5 的切片
    fmt.Println(s)      // 输出 [0 0 0 0 0],因为 int 的零值是 0

    // 使用 make 初始化一个 map
    m := make(map[string]int)
    m["key"] = 42
    fmt.Println(m) // 输出 map[key:42]

    // 使用 make 初始化一个 channel
    c := make(chan int)
    go func() {
        c <- 42
    }()
    fmt.Println(<-c) // 输出 42
}

context模块

context模块可以传递一个可取消的信号,让各个协程(goroutine)能够感知到这个取消信号,从而优雅地停止正在执行的任务。

1.方法

  • 空的Context:context.Background()创建一个空的Context,它不能被取消,也没有超时时间,也不能携带值。它通常作为顶级的Context,用于主协程。
    ctx := context.Background()
    
  • 可取消的Context:使用context.WithCancel(parent)创建一个可以被取消的Context。parent是父Context,当父Context被取消时,子Context也会被取消。
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 在合适的时候调用cancel函数来取消ctx
  • 带超时时间的Context:使用context.WithTimeout(parent, timeout)创建一个带有超时时间的Context。timeout是一个time.Duration类型,表示超时时间。
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
  • 带截止时间的Context:使用context.WithDeadline(parent, deadline)创建一个带有截止时间的Context。deadline是一个time.Time类型,表示截止时间。
    deadline := time.Now().Add(5 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()
    
  • 带值的Context:使用context.WithValue(parent, key, val)创建一个带有值的Context。key通常是一个接口类型,val是对应的值。
    ctx := context.WithValue(context.Background(), "user_id", 12345)
    

2.使用

context模块在Go语言的并发编程和分布式系统中起着非常重要的作用,它可以更好地控制协程的执行,传递请求范围的值,以及实现超时和取消机制  :

  • 网络请求:在处理HTTP请求时,context可以用来传递请求的超时时间、取消信号以及请求范围的值(如用户身份信息等)。例如,当一个HTTP请求被取消时,与这个请求相关的所有操作(如数据库查询、调用其他服务等)都可以通过context感知到取消信号,从而停止执行。
  • 分布式系统:在分布式系统中,context可以用来传递追踪ID等信息,方便进行日志记录和问题追踪。同时,也可以用来控制分布式任务的超时和取消。
  • 并发任务:在并发编程中,context可以用来控制多个协程的执行。例如,当一个任务被取消时,与这个任务相关的所有协程都可以通过context感知到取消信号,从而停止执行。
package main

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

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // 启动多个子协程来处理任务
    for i := 0; i < 3; i++ {
        go func(id int) {
            select {
            case <-ctx.Done(): // 监听上下文的结束
                fmt.Printf("Goroutine %d cancelledn", id)
                return
            case <-time.After(20 * time.Second):
                fmt.Printf("Goroutine %d finished after 20 secondsn", id)
            }
        }(i)
    }

    // 模拟任务的执行
    time.Sleep(10 * time.Second)
    fmt.Println("Cancelling all goroutines")
    cancel()
}

sync模块

sync 包是 Go 标准库中的一个非常重要的包,它提供了多种同步原语,用于在并发编程中协调多个协程(goroutine)的行为。这些同步原语可以帮助解决并发编程中的竞态条件、死锁等问题。

1.sync.Mutex

sync.Mutex 是一个互斥锁,用于保护共享资源,确保同一时间只有一个协程可以访问该资源。

var mu sync.Mutex

mu.Lock()    // 加锁,已经被获取,其它协程调用会阻塞等待
defer mu.Unlock() // 解锁

2.sync.RWMutex

sync.RWMutex 是一个读写互斥锁,允许多个协程同时读取共享资源,但写入时需要独占访问。

var rwMu sync.RWMutex

rwMu.RLock()    // 加读锁
defer rwMu.RUnlock() // 解读锁

rwMu.Lock()    // 加写锁
defer rwMu.Unlock() // 解写锁

3.sync.WaitGroup

sync.WaitGroup 用于等待一组协程完成。它通过 Add、Done 和 Wait 方法来协调协程的完成。

var wg sync.WaitGroup

wg.Add(1)    // 增加计数
defer wg.Done() // 减少计数

wg.Wait()    // 等待所有协程完成

4.sync.Once

sync.Once 用于确保某个操作只执行一次。它通过 Do 方法来保证操作的唯一性。

var once sync.Once

once.Do(func() {
    // 只执行一次的操作
})

os/signal模块

os/signal 包用于处理操作系统发出的信号。

1.基础用法

创建一个 os.Signal 类型的通道,用于接收信号:

sigChan := make(chan os.Signal, 1)

使用 signal.Notify 函数将信号通道注册到一个或多个信号上:

signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

通过监听信号通道来处理接收到的信号:

go func() {
    for sig := range sigChan {
        fmt.Printf("Received signal: %vn", sig)
        // 根据信号类型执行相应的处理逻辑
    }
}()
  • 信号忽略:signal.Ignore(syscall.SIGPIPE), 函数忽略某些信号
  • 恢复默认:signal.Reset(syscall.SIGINT),恢复默认的信号处理

2.高级技巧

可以创建多个信号通道来分组处理不同类型的信号:

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    sigChan1 := make(chan os.Signal, 1)
    sigChan2 := make(chan os.Signal, 1)
    signal.Notify(sigChan1, syscall.SIGINT)
    signal.Notify(sigChan2, syscall.SIGTERM, syscall.SIGHUP)

    go func() {
        for {
            select {
            case sig := <-sigChan1:
                fmt.Printf("Caught SIGINT: %vn", sig)
            case sig := <-sigChan2:
                fmt.Printf("Caught SIGTERM or SIGHUP: %vn", sig)
            }
        }
    }()

    fmt.Println("Program is running... Press Ctrl+C to exit")
    select {}
}

M-P-G 模型 

Go 语言的协程(Goroutine)调度机制是基于多线程实现的,但它的调度是由 Go 运行时(Go Runtime)管理的,而不是直接由操作系统内核管理。

Golang+Gin框架进行HTTP开发,记录一些学习笔记
MPG(GMP)

1.解释

M: Machine(机器 / 内核线程)

  • 对应 操作系统内核线程(OS Thread),是真正被 OS 调度、跑在 CPU 上的执行单元。
  • 数量通常不多(受 OS 线程上限、内存开销限制)。
  • 负责真正执行代码(Goroutine 的逻辑)。

P: Processor(逻辑处理器 / 上下文)

  • 代表 执行 Go 代码所需的资源与上下文(本地 G 队列、内存缓存、调度信息等)。
  • P 的数量 ≈ GOMAXPROCS(默认 = CPU 核心数),决定同时并行执行的 Goroutine 最大数量。
  • P 不直接执行代码,而是把 G 交给绑定的 M 去跑。

G: Goroutine(Go 协程)

  • 用户态轻量级线程,由 Go 运行时调度,不是 OS 线程。
  • 极小栈(初始 2KB)、可动态扩缩、创建 / 切换成本极低。
  • 每个 G 存:栈、PC(程序计数器)、状态、所属 M/P 等。

2.调度机制

  • 创建 G:go func() 生成 G,放入 P 本地队列 或全局队列。
  • M 绑定 P:空闲 M 从全局 P 队列获取一个 P,形成 M-P-G 执行单元。
  • 执行 G:M 从 P 队列取 G 执行。
  • G 阻塞(I/O/channel/ 锁)
  • M 会解绑当前 P。
  • P 去找另一个空闲 M 继续执行其他 G(不浪费 CPU)。
  • 阻塞的 G 被挂起,等事件就绪后重新入队。
  • G 主动让出(runtime.Gosched):放回队尾,P 执行下一个 G。
  • P 本地队列为空:去全局队列或其他 P 的队列偷 G(work-stealing)。

go:embed

把文件 / 目录在编译时,直接打进 Go 可执行文件里,运行时直接读取,不需要外部文件。

  • 打包静态资源(HTML、CSS、配置)
  • 内置版本号、标识、密钥
  • 单文件分发,不依赖任何外部文件
  • 可编译后修改二进制内容(你要的功能)

1.支持的 3 种类型

嵌入字符串(文本文件):

//go:embed config.json
var config string

嵌入字节(图片、二进制、加密文件):

//go:embed logo.png
var logo []byte

嵌入文件系统(目录、多文件):

// 嵌入两个目录内的所有文件
//go:embed static/* templates/*
var files embed.FS

// 读取
data, _ := files.ReadFile("static/index.html")

开发经验

记录自己Golang开发过程中解决过的问题

1.有缓冲通道

类型 定义方式 发送条件 核心用途
无缓冲通道 make(chan T) 必须有接收方在等 协程同步、通知、等待
有缓冲通道 make(chan T, n) 缓冲区未满即可发送 异步队列、限流、生产者消费者
  • 无缓冲通道 = 必须有人等,才能收发,同步阻塞
  • 有缓冲通道 = 自带 “队列”,缓冲区没满就能直接发,异步
# 无缓冲通道
ch := make(chan int)

// 发送
go func() {
    ch <- 1 // 会阻塞,直到有人 <-ch
}()

// 接收
<-ch // 取到值,上面的 goroutine 才继续

----------------------------------------------

# 有缓冲通道
ch := make(chan int, 2)

ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
ch <- 3 // 阻塞,因为缓冲区满了

2.struct可选字段?

最常用、最推荐的写法,直接用指针表示 “可选”:

type Config struct {
    Host    string  // 必填
    Port    int     // 必填
    Timeout *int    // 可选
}

用指针做可选字段的原理:

  • 普通类型永远有值(零值),无法判断是否被设置
  • 指针可以是 nil,代表 “未设置 / 不存在”
  • 指针非 nil,代表 “已设置 / 存在”

这是 Go 语言区分「有 / 无」的唯一天然方式

Go 里的 struct 初始化时,没写的字段,不会是 “不存在”,而是自动赋上「默认零值」。

3.匿名函数使用外部作用域的变量

go 关键字 = 创建并启动一个 Go 协程(goroutine)

Go 的匿名函数(go func)完全可以直接使用外部作用域的变量。

如果是用于创建并启动协程,建议用参数传递,不直接用外部变量,防止出严重 Bug(数据错乱、panic)。

4.自动分号插入

Go 编译器的自动分号插入(Automatic Semicolon Insertion, ASI) :在换行符前,如果最后一个 token 是以下可结束语句的符号,编译器会自动插入分号:

  • 标识符(变量名 / 函数名)
  • 数字、字符串、字符常量
  • + - * / % 等运算符
  • 关键字:break continue return ++ --
  • 右括号:) } ]
// ✅ 正确(最后一行加逗号,阻止 ASI)
arr := []int{
    1,
    2,
    3, // 必须加
}

// ❌ 错误(编译器在 3 后自动插分号,语法断裂)
arr := []int{
    1,
    2,
    3
}

Go 1.13 至今(现代 Go)核心逻辑完全没变,只有一种情况可以省略最后一个逗号,右花括号 } 和最后一个元素在同一行 :

// ✅ Go 1.13+ 允许(} 和元素同行,省略逗号)
arr := []int{1, 2, 3}

// ✅ 分行写:无论哪个版本,最后一行【必须加逗号】
arr := []int{
    1,
    2,
    3, // 必须加
}

5.通道类型

Go 通道读写权限控制的语法,专门用来限制通道只能读 / 只能写,避免并发错误。

语法 名称 权限 记忆口诀
chan T 双向通道 可读、可写 无箭头 = 全能
<-chan T 只读通道 只能读,不能写 箭头在左 = 只能收数据
chan<- T 只写通道 只能写,不能读 箭头在右 = 只能发数据
  • T = 通道里传输的数据类型(如 int、string、struct)
  • 箭头方向就是数据流动方向

不同类型的通道转换规则:

  • 双向通道 → 可以自动转成 只读 或 只写
  • 只读 / 只写 → 永远不能转回 双向通道
// 生产者结构体
type Producer struct {
    out chan<- string // 只允许发消息
}

// 发消息
func (p *Producer) Send(s string) {
    p.out <- s
}

// 消费者结构体
type Consumer struct {
    in <-chan string // 只允许收消息
}

// 读消息
func (c *Consumer) Read() string {
    return <-c.in
}

ch := make(chan string)

// 同一个通道,分别赋给不同结构体
p := Producer{out: ch}
c := Consumer{in: ch}

6.io.Pipe

io.Pipe 是 Go 标准库 io 包提供的内存管道,核心作用是在两个 goroutine 之间实现无缓冲的同步数据流传输:一个协程往管道写数据,另一个协程从管道读数据,读写是阻塞同步的,不需要临时文件或缓冲区。会返回两个对象:

  • *io.PipeWriter:写入端,实现了 io.Writer 接口
  • *io.PipeReader:读取端,实现了 io.Reader 接口
package main

import (
	"io"
	"log"
	"os"
)

func main() {
	pipeReader, pipeWriter := io.Pipe()

	go func() {
		defer pipeWriter.Close()
		
		_, err := pipeWriter.Write([]byte("测试"))
		if err != nil {
			log.Fatal(err)
		}
	}()

	_, err := io.Copy(os.Stdout, pipeReader)

	if err != nil {
		log.Fatal(err)
	}

}

喃喃自语

记录使用golang过程中的一些感悟。

1.2026-04-08

复习Goalng面向对象的一些写法,接收器(Receiver) =  这个方法属于谁

func (r *Receiver) pick(a string) string{}

复习Golang哪些类型是引用类型,引用类型本身保存的是指针:

map、slice、channel

复习Golang的指针,语法和用途跟c的差不多:

&a   //获取指针
*p //声明指针类型
*p = 1  //修改指针指向的数据

2.2026-04-17

格式化字符串:

import (
     "fmt"
)
fmtString = fmt.Sprintf("%s, %s" , "是的" ,"是的")

log.Fatalln 和 log.Fatal 功能几乎一样,只有一个小区别:

  • log.Fatal(内容)直接输出内容,不自动加换行,多个参数之间不加空格
  • log.Fatalln(内容)输出内容后自动加换行多个参数之间自动加空格

启动协程:

go func(){

}()   #此处需要调用

函数返回多个参数时,也要按照返回参数顺序来接收:

// 名称和实际是错误的
pipeWriter , pipeReader = io.Pipe()

// 正确的
pipeReader,pipeWriter := io.Pipe()