Golang速通(Javaer版)

GoLang

基础语法

变量和常量

变量声明和变量类型

  • Go 在不同类型的项之间赋值时需要显式转换

  • 可以使用 **fmt.Printf("v is of type %T\n", v) **这个%T来打印变量v的类型

常量声明的两种方式

不能把一个函数的返回值赋给常量,因为函数的返回值是运行时确定的,我们只可以给常量一个编译期就确定的值

无类型常量(隐式类型声明 ):const Pi = 3.14,此时 Pi ** 没有固定类型,它可以根据上下文自动转化为 float32float64,甚至是自定义的浮点类型**

  • 直到它被真正使用(赋值给变量或作为函数参数)时,编译器才会根据上下文环境(Context)来决定它的具体类型

显式类型声明const Pi float64 = 3.14159

Go的枚举实现------iota 计数器

Go没有枚举类型,但是有iota 计数器, const 关键字与 iota 标识符的配合就可以实现枚举类型了

iota 是 Go 的一个预定义标识符 。它的本质是一个编译期修改的常量计数器

  • const 关键字出现的行,iota 被重置为 0。随后,在同一个常量声明块中,每新增一行,iota 的值就会自动加 1

  • 隐式重复 :Go 的常量声明有一个特性------如果一行没有写表达式,它会复用上一行的表达式(这在某些复杂的声明里面可能是一个坑)

Go 复制代码
const (
    Read    = 1 << iota // iota = 0, 结果 001 (1)
    Write               // 自动复用表达式 1 << iota, iota = 1, 结果 010 (2)
    Execute             // 自动复用表达式 1 << iota, iota = 2, 结果 100 (4)
    Admin               // 自动复用表达式 1 << iota, iota = 3, 结果 ???
)

控制流语句

if条件控制语句

Go 的 if 语句与 C 或 Java 相比,最显著的区别是不需要括号,但必须有大括号

Go 复制代码
if condition {
    // 执行代码
}else if condition2{
    //执行代码
}else{
    //就算只有一行,也必须写这个大括号
}
  • **Go 的特色:初始化语句->**可以在 if 关键字后面直接声明一个变量,这个变量的作用域仅限于该 if-else

    Go 复制代码
    if num := 10; num > 0 {
        fmt.Println(num, "是正数")
    }
    // 这里无法访问 num

    现代 CPU 都有分支预测器。如果你的 if 条件极度不平衡(例如 99% 的情况都走某一个分支),CPU 会预先加载该路径的指令

switch 语句

在 C/Java 中,如果你忘记在 case 结尾写 break,程序会继续执行下一个 case(即穿透)。但在 Go 中:

  • 默认不穿透 :每个 case 执行完会自动跳出 switch,不再需要手动写 break

  • 显式穿透如果你确实需要穿透逻辑,必须显式使用 fallthrough 关键字

    Go 复制代码
    switch level {
    case 3:
        fmt.Println("步骤 3:高级系统配置")
        fallthrough // 强制进入下一个 case
    case 2:
        fmt.Println("步骤 2:中级性能优化")
        fallthrough // 继续穿透
    case 1:
        fmt.Println("步骤 1:基础环境检查")
    default:
        fmt.Println("任务完成")
    }
    • fallthrough 必须是 case 块中的最后一条语句。如果它后面还有代码,编译器会报错

for循环控制

Go 只有 for 这一种循环结构,而且 **for**循环也是没有括号的

  • 基础的三段式的for循环,这个for循环和C/Java等语言类似

    Go 复制代码
    for i := 0; i < 10; i++ {
        // 逻辑代码
    }
  • 省略了前置和后置条件就变成了"伪 while" 循环

    Go 复制代码
    for condition {
        // 只要条件成立就一直执行
    }

复杂数据类型

切片(Slice)

在 Go 中,数组的长度是类型的一部分且固定不变(因此获取数组长度是O(1)的操作,不需要像C语言一样进行遍历),这在处理动态数据时非常不便

切片看起来像动态数组(常见的使用场景) ,但底层其实是一个结构体------****SliceHeader

在运行期间,每个切片实际上由三个部分组成:

  • 因此传递大切片非常高效------因为它只传递了这 16 或 24 字节的结构体,而不是整个数据集

  • 可以使用 **len(s) cap(s)**函数来获取长度和容量信息


动态扩容:append 的策略

当你往切片里 append 元素导致超过 Cap 时,Go 会自动扩容:分配一块更大的新内存->将旧数据拷贝过去->返回指向新数组的新切片

底层算法(Go 1.18+ 优化版)

  • 如果当前容量小于 256,则翻倍(2×2\times2×)

  • 如果大于 256,则按照 (prev_cap+3×256)/4(prev\_cap + 3 \times 256) / 4(prev_cap+3×256)/4 的公式平滑增加,直到满足需求。

func append(slice []Type, elems ...Type) []Type这是append函数的API

  • slice:这是你要操作的源切片

  • elems :这是你要追加的一个或多个元素,其类型必须与切片的元素类型一致

  • 返回值:它会返回一个新的切片

Go 复制代码
func bad(s []int)  { s = append(s, 99) }   // 改的是局部副本的 header,白改
func good(s []int) []int { return append(s, 99) } // 必须 return
func ptr(s *[]int) { *s = append(*s, 99) } // 或者传指针

上面的代码块就很好的演示了slice的本质是三个元素构成的结构体,第一个bad函数因为传入的是s而不是引用,因此是值传递,因此在函数体里面修改s的操作仅仅会影响这个s的副本;第二个因为good使用return出去了因此是可以保存改变的,但是需要外面有变量进行承接;第三个比较特殊主要的原因是因为传入的不是值而是原来的引用,因此函数体里面的改动会直接影响到外面的


  1. 直接声明(零值切片):最简单的方式是只声明类型而不初始化 **var s []int****,这里的切片s其实就是一个nil切片,nil切片是可以直接进行append操作的(对应的len=0,cap=0)**

    • 所有 len=0 的非 nil 切片,指针都指向同一个全局变量 runtime.zerobase,不占额外内存。
  2. 字面量初始化:在声明的同时赋予初始值 **s := []int{10, 20, 30}**

  3. 使用 make 函数初始化:****s := make([]int, 5, 10) // 长度为5,容量为10

    • 它可以预分配内存,避免频繁扩容带来的性能损耗

    • a := make([]int, 5) // len(a)=5,这个不会指定容量,但是默认容量和长度相等了


因为切片其实本质上和指针是类似的,因此其就像是数组的一个视图,一个数组可以有多个切片,而且他们都对数组有修改的权限

  • s1 := a[1:3] (从索引 1 截取到 3,不包含 3),切片都是左闭右开的

  • 切片是会影响原来的数组的,这就是为什么叫作视图的意义(但是主要的原因还是ptr)

    Go 复制代码
    a := []int{1, 2, 3, 4, 5}
    s := a[0:2]              // s=[1,2], len=2, cap=5(有余量!)
    s = append(s, 100)       // len<cap → 原地写到下标2
    fmt.Println(a)           // 输出: [1 2 100 4 5]  ← a[2] 被悄悄改了!
    fmt.Println(s)           // 输出: [1 2 100]

    这里的原因是因为s:=a0:2表示的是ptr=a,len=2,cap=5 ,然后因为len<cap因此不会去开辟一块新的地方,但如果len=cap的话对应ptr就装不下这么多数据因此这个ptr就不再指向a了,因此就不会改变原来的数组a

  • 三索引切片 a[0:2:2] a[low:high:max]而不是Python模式下的 a[start:stop:step],这样主要的作用是把 cap 也焊死成 2,而不是根据a的具体长度去进行变化的cap=a.length-start的默认模式

    Go 复制代码
    s := a[0:2:2]
    // low=0, high=2, max=2
    // len = high - low = 2
    // cap = max  - low = 2   ← 第三个数是用来限制 cap 的!
    s := a[0:2] // 这种写法如果a的长度就是2的话那么cap=2,但是如果a的长度是5的话那么cap=5

哈希表(Map)

一种存储 键值对(Key-Value pairs) 的无序集合

几种常见的声明方式:

  • 字面量初始化 :直接赋予键值。 m := map[string]int{"Alice": 25, "Bob": 30}

    • 这表示key的类型是string,然后值的类型是int
  • 使用 make 函数 :预分配空间以提高性能。 m := make(map[string]int, 10) // 预估存储 10 个元素

  • 零值 Mapvar m map[string]int注意 :此时 mnil你可以从 nil Map 中读取数据,但如果尝试写入,程序会直接 Panic(崩溃)

    • nil map:读没事,写 panic

      Go 复制代码
      var m map[string]int   // 只声明,没初始化 → nil map
      fmt.Println(m["x"])    // 输出: 0    ← 读 nil map 合法,返回零值
      fmt.Println(m == nil)  // true
      m["x"] = 1             // 💥 panic: assignment to entry in nil map
      • 这里因为返回0值是默认操作,因为Go的Map不是像Java一样的对象,因此没有办法直接返回nil,因此就需要用Ok那个双返回值来进行处理了。写panic的原因是因为还没有初始化分配地址呢

      • Go 的 nil map 写 panic 是一个道理------只是 Go 读 nil map 不报错(返回零值),这点比 Java 宽松。


Map 实际上是一个 哈希表(Hash Table) ,它的底层结构体是 hmap

  • hmap : 记录了整个 Map 的元信息,比如当前的元素数量 count、桶的数量(以 $

    2^B$ 表示)以及指向桶数组的指针

  • bmap (Bucket) : 这是存储数据的基本单元。每个桶里固定存放 8 个键值对

    • 为了优化内存对齐,桶的内部构造是:先放 8 个 Key,再放 8 个 Value,最后是溢出桶的指针

  • 由于读取不存在的 Key 会返回零值,我们无法仅通过 v 来判断一个 Key 是否真的存在(比如这个value类型里面存的本身就是0呐),因此我们可以使用双返回值来进行判断

    Go 复制代码
    value, ok := m["key"]
    if ok {// Key 存在
    } else {// Key 不存在
    }
    • Java 的 null 方案其实是有缺陷的

      Java 复制代码
      Map<String,Integer> m = new HashMap<>();
      m.put("a", null);        // Java 允许 value 存 null
      m.get("a");   // 返回 null ------ 到底是"没这个key"还是"值就是null"?分不清!
      • 就是因为这里返回null到底是不是值的原因导致有二义性,因此后来补充了API------ containsKey() 和 getOrDefault(),而Go一开始就考虑好了的
  • Java有显式并发容器(ConcurrentHashMap),而且Java 没有「跨线程实时检测数据竞争」的廉价机制。它只有 modCount 做迭代器的 fail-fast(单线程遍历时被改会抛 ConcurrentModificationException),但那是给迭代用的,管不了两个线程同时 put,因此Go就不需要处理这些Go的不用创建一个并发的HashMap

  • 并发写Map直接Panic崩溃

    Go 复制代码
    m := map[int]int{}
    go func() { for { m[1] = 1 } }()  // 一个 goroutine 写
    go func() { for { _ = m[1] } }()  // 另一个读
    // 💥 fatal error: concurrent map read and map write
    • Go runtime 在 map 结构里埋了个写标志位。一个 goroutine 写时打标,别的 goroutine 进来发现标志位被占,直接 fatal error 把整个进程干掉。

    • fatal error,不是 panic。连recover() 都救不回来,比普通 panic 更狠

    • 而Java并发读写HashMap是会数据错乱的,但是不一定崩,但是是Go 故意设计为崩溃------因为并发读写 map 是 bug,runtime 宁可让你当场崩,也不让你带着脏数据裸奔。

  • 并发读写的方案:

    • sync.RWMutex + 普通 map(推荐方案)

      Go 复制代码
      type SafeMap struct {
          mu sync.RWMutex
          m  map[string]int
      }
      
      func (s *SafeMap) Get(k string) (int, bool) {
          s.mu.RLock()         // 读锁,多个读可并发
          defer s.mu.RUnlock()
          v, ok := s.m[k]
          return v, ok
      }
      
      func (s *SafeMap) Set(k string, v int) {
          s.mu.Lock()          // 写锁,独占
          defer s.mu.Unlock()
          s.m[k] = v
      }
    • sync.Map(适用于读多几乎不写的场景下,或者各个goroutine不会操作同一个锁的场景)不像 ConcurrentHashMap那样全能

  • 并发扩容机制:Java ConcurrentHashMap 支持「多线程协同扩容」,Go map 是「单线程渐进式扩容 + 完全不支持并发」

    • Go语言:分配 newbuckets,保留 oldbuckets 指针,但不一次性搬完。之后每次写操作(assign/delete)顺手迁移 1~2 个旧桶到新桶。读的时候新旧桶都查。

    • Java语言:transfer() 支持多线程帮忙搬------**每个线程通过 transferIndex 认领一段桶区间并行迁移,迁移完的桶放 ForwardingNode 标记,其他线程读到这个标记会转到新表。**这是真正的并发扩容

  • 哈希表遍历的特征------Go的for...range遍历Mao是遍历随机的

    • Java HashMap:遍历顺序由桶下标 + 桶内插入顺序决定。它看起来杂乱,但对同一个没被修改的 map 实例,多次遍历顺序是稳定、可复现的。它只是「无规律」,不是「每次都变」。

    • Go map:runtime 在 range 启动时故意塞一个随机起始桶 + 随机起始槽位,所以同一个 map 连续遍历两次,顺序都可能不同。这是 Go 刻意设计的------就是要防止你写出依赖遍历顺序的代码。

    • Java = 顺序无规律但同实例稳定;Go = 每次遍历主动随机化。

函数

一个标准的 Go 函数声明包含函数名、参数列表、返回值类型以及函数体:

Go 复制代码
func functionName(param1 type1, param2 type2) returnType {
    // 函数体
    return value
}
  • Go 的函数有几个非常实用的特性:

    • 多返回值:一个函数可以返回多个结果->最常见的就是错误处理里面

    • 变长参数 :使用 ...type 语法可以接受任意数量的同类型参数

    • 命名返回值 :你可以在函数头部定义返回变量的名字,这样在函数体内直接修改这些变量,最后只需调用 return 即可

      Go 复制代码
      func swap(x, y int) (first, second int) {
          first = y
          second = x
          return // 这里的 return 不需要再显式写变量名,被称为"裸返回(Naked Return)"
      }
      • 通过变量名直接告诉调用者返回值的含义,比单纯的类型(如 (int, int))更清晰
  • **函数也是值。它们可以像其他值一样传递。**函数值可以用作函数的参数或返回值(函数式变成)

匿名函数

匿名函数就是没有名字的函数。它可以被定义在另一个函数内部,并立即执行或赋值给一个变量

Go 复制代码
func main() {
    // 定义并立即执行
    func(msg string) {
        fmt.Println(msg)
    }("Hello Go!")
}

定义了匿名函数他不会自己执行,必须我们配置一个()来进行调用执行,就算没有参数也是需要一个()的

函数的闭包

闭包 是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予被引用变量的值,即使该变量已经超出了它的原始作用域

GoLang的闭包其实是Rust闭包的子集,简单使用而且不需要考虑那么多

Java 的 lambda 捕获局部变量要求变量 effectively final(捕的是值的快照,不准改);Go 闭包捕的是变量本身,能读能写。这是最大的区别

简单来说:闭包 = 函数 + 引用环境

Go 复制代码
func makeMultiplier(factor int) func(int) int {
    // factor 是外部变量
    return func(n int) int {
        return n * factor // 内部函数捕获了 factor
    }//返回值是一个func(int) int的函数,因此只要这个函数没死,那么这里的factor也不会死
}

普通的函数中,局部变量在函数返回后就会被销毁。但在闭包中,**如果内部函数引用了外部函数的局部变量,Go 编译器会通过逃逸分析 (Escape Analysis) 将该变量分配到堆上,使其生命周期延长,直到闭包不再被使用(**因为一般函数都是在栈里面处理所有的变量的,但是闭包给了一种机制,让你栈里面的数据如果在外面会被用到,那么就会自动放到堆里面去存着,算一种语法糖)

Go 复制代码
func main() {
    var funcs []func()
    for i := 0; i < 3; i++ {
        funcs = append(funcs, func() {
            fmt.Println(i) // 每个匿名函数都捕获了变量 i
        })
    }

    for _, f := range funcs {
        f() // 循环结束后再统一执行这些闭包
    }
}

闭包捕获变量是按引用(Reference)进行的:因此上面这个例子打印的结果是3,3,3而不是按照值传递的1,2,3。这些闭包在创建时捕获的是同一个 i 的内存地址,所以当循环结束、它们真正开始执行时,都会去那个地址读取当时的值,也就是3。但是后来Go改变了这种设计:

Go ≤ 1.21 输出:3 3 3;Go ≥ 1.22 输出:0 1 2

  • 老版本里,**i 是整个 for 循环共享的一个变量(一个地址)。三个闭包抓的都是同一个 i 的地址。**等循环结束,i 已经变成 3,三个闭包一起去读那个地址 → 全是 3。

  • Go 1.22 改了语义:每次迭代都创建一个全新的 i。三个闭包各抓各的那一轮的 i,互不干扰 → 0 1 2,这个也更符合我们的直觉

1.22 修的是「for 循环变量每轮复用」这一个特定 bug,不是「闭包捕获」这个机制。 闭包永远抓的是「变量本身」,只要多个闭包/goroutine 共享同一个变量

Go 复制代码
sum := 0
funcs := []func(){}
for i := 0; i < 3; i++ {
    sum += i
    funcs = append(funcs, func() { fmt.Print(sum, " ") })
}
for _, f := range funcs { f() }
// 1.22 依然输出 3 3 3 ------ 因为 sum 是循环外的同一个变量,三个闭包抓的还是它

因为这里的sum不是循环里面的变量而是循环外面的变量,因此还是会显示相同的值的,因此如果变量不是循环变量的话,那么就必须要使用值传递的方案去修复了,比如下面的方案:

Go 复制代码
sum := 0
funcs := []func(){}

for i := 0; i < 3; i++ {
    sum += i

    currentSum := sum // 每一轮创建一个新的局部变量

    funcs = append(funcs, func() {
        fmt.Print(currentSum, " ")
    })
}

for _, f := range funcs {
    f()
}

延迟执行函数 (defer)

  • 主要作用是延迟执行,defer 语句后面的函数不会立即执行,而是会被推入一个栈中。只有当外层函数执行到 return 或者发生 panic(崩溃)时,这些延迟函数才会被调用

  • 如果一个函数里有多个 defer,它们会按照后进先出(Last In, First Out)的顺序执行

  • defer 后面函数的参数值,在 defer** 语句出现的那一刻**就已经确定了,而不是在函数真正执行时才计算

    • 这里需要注意,这个确定好的参数值是一个指针(一般是因为闭包)还是一个普通的值
    Go 复制代码
    func main() {
        i := 1
        // 这一刻,Go 记住了 i 是 1
        defer fmt.Println("直接传参:", i) 
        
        i = 100
        // 函数结束时,打印的是 1 还是 100?
    }

    defer 后面的函数如果是带参数的(比如 fmt.Println),Go 会在执行到 defer 这一行时,立刻完成参数的值拷贝 。就像拍了一张照片,之后 i 怎么变,照片里的数字都不会变

    Go 复制代码
    func main() {
        i := 1
        // 这一刻,Go 记住了 i 的"家庭地址"
        defer func() {
            fmt.Println("闭包捕获:", i) 
        }()//因为是闭包,所以会记住的是i的地址而不是值
        
        i = 100
        // 函数结束时,它顺着地址去看,发现 i 已经变成 100 了
    }

    defer func() { ... }() 时,你调用的是一个没有参数 的函数(因为没有参数,在调用的那一刻,并没有任何 被复制)。函数体内部的代码只有在最后执行时,才会顺着那根"线"去寻找变量 i 当前的值,所以才会导致记住的是地址不是值

defer 执行在 return 中间------return 不是原子的,它分两步。defer 夹在中间。

return x在Go里面被翻译成三步:

  • 第 1 步:把返回值赋值(给返回变量)

  • 第 2 步:执行所有 defer

  • 第 3 步:真正返回到调用方

  • 因此这样就有两种情况:

    1. 如果用了具名返回值,defer 可以在返回前把它改掉

      Go 复制代码
      func f() (result int) {   // ← 返回值有名字:result
          result = 5
          defer func() { result++ }()
          return result
      }
      // 返回 6
    2. 匿名返回值,defer 改不了:因为返回值没有名字

      Go 复制代码
      func g() int {
          result := 5           // ← 这个 result 是普通局部变量,自己起的名
          defer func() { result++ }()
          return result
      }
      // 返回 5

      **函数体里的 result 只是你自己声明的一个普通局部变量,它跟返回槽是两个东西。**return result 会把 result 的值(5)拷贝进一个你无法用名字访问的匿名返回槽。之后 defer 改的是局部变量 result(改成 6),但返回槽里已经是拷过去的 5 了,跟局部变量脱钩→ 返回 5。

面向对象

结构体 (Struct)

在 Go 语言中,没有像 Java 或 Python 那样的 class结构体 是我们将不同类型的数据组合在一起、构建复杂对象的唯一方式。它是面向对象编程在 Go 中的基石

  1. 结构体定义与实例化:type User struct { Name string; Age int }

    1. 导出与私有(可见性)

      • 如果字段名首字母大写 (如 Title),它是导出的(Public),其他包的代码可以访问它

      • 如果首字母小写 (如 price),它是私有的(Private),只能在当前包内使用

    2. 初始化

      Go 复制代码
      type Vertex struct {
              X, Y int
      }
      
      var (
              v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体,必须按顺序赋值
              v2 = Vertex{X: 1}  // Y:0 被隐式地赋予零值
              v3 = Vertex{}      // X:0 Y:0
              p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
      )
      • 可以使用Name: 语法可以初始化列出部分字段,剩下的走默认值(或者零值)
    3. 直接使用.号来进行获取,Go删除了->访问指针的方式,而是走了Java那种指针和值统一的方式

    4. 结构体嵌套和字段提升

      Go 复制代码
      type Admin struct {
          User  // 匿名嵌套,Admin 自动拥有了 User 的所有字段
          Level int
      }

    如果你在嵌套时不给字段起名字 ,它就变成了匿名成员。**然后你可以直接通过 admin.Name 访问 User 里的成员,而不需要写成 admin.User.Name 。**这让 Go 的组合(Composition)用起来非常像继承

    • 当我们在结构体中使用匿名嵌套(Embedding)时,确实可能会遇到"同名冲突"的情况。这种现象通常被称为字段遮蔽(Field Shadowing)

      • 当你通过 admin.Name 访问时,Go 会优先寻找 Admin 自身定义的字段。内部的字段并没有消失,只是被"藏"起来了,你仍然可以通过完整的路径 admin.User.Name 访问它

      • 如果一个结构体同时嵌套了两个结构体(比如 S1S2),且它们都有一个字段叫 X,而外部结构体没有定义 X

        • 当你尝试访问 obj.X 时,Go 编译器会报错,提示 ambiguous selector(歧义选择器)

        • 编译器不会帮你猜你想用哪一个,它要求你必须显式指定路径,如 obj.S1.Xobj.S2.X

  2. 结构体方法 (Methods) :func (u User) SayHi() { ... }

    1. 结构体方法是需要进行类型的绑定的,也就是这里的(u User)

    2. 指针接收者 vs 值接收者

      • 值接收者 ****(u User) :方法执行时会得到 User 的一个副本 (拍照)。你在方法里修改属性,不会影响原来的对象

      • 指针接收者 ****(u *User) :方法执行时拿到的是地址 (钥匙)。你在方法里修改属性,原对象会跟着变

  3. 结构体标签(Struct Tag):

    • 在 Go 里面,结构体的字段名通常是大写开头的(为了跨包可见),比如 UserName 。但前端的 JSON 可能要求小写 user_name ,或者数据库里叫 u_name,因此需要建立一种映射关系,用来告诉框架一些映射的信息

    • 标签写在字段定义的后面,用反引号 ` 包裹。格式一般都是 **key:"value"**的形式

      C++ 复制代码
      type User struct {
          // json: 定义 JSON 序列化的名称
          // binding: 定义 Gin 的参数校验规则
          Name string `json:"user_name" binding:"required"`
          Age  int    `json:"age" binding:"gte=1,lte=120"`
      }

接口(interface{}

接口是 Go 语言实现解耦多态 的核心工具。与结构体不同,接口不关心对象"是什么"(数据),而只关心对象"能做什么"(行为)

  • 接口是一组方法签名的集合。如果一个结构体实现了接口中要求的所有方法,我们就说这个结构体"实现"了该接口

    Go 复制代码
    // 定义接口
    type Speaker interface {
        Speak() string
    }
    
    // 结构体 A:狗
    type Dog struct{}
    func (d Dog) Speak() string { return "汪汪!" }
    
    // 结构体 B:机器人
    type Robot struct{}
    func (r Robot) Speak() string { return "你好,人类。" }

    这里就会说Dog和Robot这两个结构体都实现了接口Speaker

    Go 复制代码
    func Announce(s Speaker) {
        fmt.Println("广播内容:", s.Speak())
    }

    这样我们可以将接口当作参数传入函数里面,这样只要实现了这个接口的结构体对象就都可以调用这个函数了

  • 空接口interface{}:空接口是所有结构体都实现了的接口

    • fmt.Println 为什么能打印任何东西?因为它接收的就是 ...interface{}

    • 当你想接收某些数据但是不知道其类型的时候,那么就可以使用空接口来承接

类型断言(Type Assertion)

空接口能装任何东西,但当你把它拿出来用时,由于它是"隐形的",编译器不知道它到底是什么,这个时候就需要使用类型断言来"拆箱"

Go 复制代码
var x interface{} = 10

// ❌ 报错:invalid operation: x + 5 (mismatched types interface{} and int)
// fmt.Println(x + 5) 

// ✅ 必须断言
if v, ok := x.(int); ok {
    fmt.Println(v + 5) // 正常输出 15
}

我们尝试对x这个对象转换成int,如果成功了的话,那么这个ok就是true,那么我们就可以直接将anythong当作int来使用。如果不写这个断言进行拆箱那么我们就不可以将其当作int使用(哪怕其其实本身是可以当作int使用的)

接口的组合

它允许我们将多个微小的接口,"组合"成一个功能更强大的新接口

Go 复制代码
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// *ReadWriter 组合了 Reader 和 Writer*
type ReadWriter interface {
    Reader
    Writer
}

任何实现了 ReadWrite 方法的结构体,都会自动同时满足 ReaderWriterReadWriter 这三个接口

接口的具体应用
Java 复制代码
func OrderDone(n Notifier) {   // 参数是接口
    n.Send("您的订单已完成 ✅")
}

func main() {
    OrderDone(SMSSender{})    // 📱 短信: 您的订单已完成 ✅
    OrderDone(EmailSender{})  // 📧 邮件: 您的订单已完成 ✅
    OrderDone(WechatSender{}) // 💬 微信: 您的订单已完成 ✅
}// OrderDone只关心一点就是你能不能 Send,有没有这个能力,只要有就可以了

/// 然后我们就只需要对应的结构体都有这个Send这个方法就可以了
type SMSSender struct{}
func (s SMSSender) Send(msg string) { fmt.Println("📱 短信:", msg) }

type EmailSender struct{}
func (e EmailSender) Send(msg string) { fmt.Println("📧 邮件:", msg) }

type WechatSender struct{}
func (w WechatSender) Send(msg string) { fmt.Println("💬 微信:", msg) }

解耦的回报:新增能力靠「加类型」,而不是「改逻辑」。这里就起到了一个一次写好多次扩展的能力,以后需要添加一种新的方式来处理订单我们只需要那个结构体有Send的能力即可

结构体(struct)是「数据是什么」,接口(interface)是「能做什么」。

嵌入

Java 里一个 extends 同时承担了两个完全不同的职责------复用父类的代码结构+子类可以当作父类使用(多态)

Go 把这两件事拆成了两个独立机制:

  • ① 代码复用 → 用组合 / 嵌入(composition / embedding)

  • ② 多态 → 用接口(interface)

Go 复制代码
type Animal struct {
    Name string
}
func (a Animal) Eat()   { fmt.Println(a.Name, "在吃东西") }
func (a Animal) Sleep() { fmt.Println(a.Name, "在睡觉") }

type Dog struct {
    Animal   // 👈 匿名嵌入,注意没有字段名!
}
func (d Dog) Bark() { fmt.Println(d.Animal.Name, "汪汪") }

把结构体当成一个字段塞进去,只是写的时候不写字段名,这个叫匿名嵌入,主要的目的就是为了少些一点调用的逻辑:

  • 如果你写成具名字段 animal Animal,那你就得 d.animal.Eat()

  • 但是如果没有写字段名那么Go 会自动做方法提升(method promotion),d.Eat() 就直接相当于d.Animal.Eat()

错误处理

Go 的错误处理非常直截了当:它不使用 try-catch 这种捕获异常的机制,而是坚持"错误也是一种值"。这意味着函数在返回结果的同时,通常也会返回一个 error 类型的值

Go 复制代码
f, err := os.Open("filename.ext")
if err != nil {
    // 处理错误(比如打印日志或返回给调用者)
    return err
}
// 只有 err == nil 时,才安全地使用 f
  • try-catch 会导致逻辑跳转变得难以追踪。通过将错误显式返回,Go 迫使开发者在错误发生的第一现场 决定如何处理它。这种"显式优于隐式"的做法让代码的路径非常清晰,但是也会导致写起来非常难受会有很多的 **if err != nil**

error 本身其实是一个接口。它的定义极其简单:

Go 复制代码
type error interface {
    Error() string
}
  • 这意味着任何实现了 Error() string 方法的结构体都可以作为一个错误

泛型

Go 1.18 引入的一个"重量级"特性:泛型 (Generics)

泛型允许我们在定义函数、结构体或接口时,先不指定具体的类型,而是使用一个类型形参 (Type Parameter)。你可以把它想象成给类型预留了一个"占位符"

Go 复制代码
// [T any] 是泛型的核心。T 代表某种类型,any 表示它可以是任何类型。
func PrintAnything[T any](item T) {
    fmt.Println(item)
}

泛型解决了代码冗余的问题,同时保持了类型安全

类型约束

如果你定义 [T any],那 T 确实可以是任何类型。但如果你想在函数里做一些特定的操作(比如比较大小 > 或相加 +),你就必须限制 T

Go自带一些类型约束比如comparable,但是我们也可以自定义类型约束

Go 复制代码
type Number interface {
    int | int64 | float64
}

// 只有满足 Number 接口的类型才能调用这个函数
func Add[T Number](a, b T) T {
    return a + b
}//
Go 复制代码
type MyInt int

type Ordered interface {
    ~int | ~float64 | ~string
}//~int。它的意思是:**不仅匹配 int,还匹配所有底层类型是 int 的自定义类型,这样MyInt就匹配了**

Go泛型实现的原理

Go 泛型是 Go 1.18(2022)引入的,比 Java 晚很多,实现是一种编译期为主的折中方案,介于「完全单态化」和「类型擦除」之间

  • 用接口来表达约束,any 就是 interface{}。还引入了 ~(底层类型------int64的底层类型是int)和类型集合(type sets------可以使用集合运算|等符号来运算)来描述「哪些类型允许」

  • Go 编译器不是给每个具体类型都生成一份代码(那会代码爆炸),也不是完全擦除。它按「GCShape」分组:

    • 所有指针类型共享同一份实例化代码(因为它们在 GC 和内存布局上形状相同)。

    • 值类型则按各自的形状分别生成代码(如 int、float64、不同大小的 struct)。

  • 对于共享同一份代码的那些类型(比如各种指针),编译器在调用时额外传入一个隐藏的「字典」参数,里面装着具体类型的元信息(类型描述符、需要调用的方法等),让同一份机器码能服务多个类型。因为指针虽然具体的内存格式是一样的,但是不知道这个指针的服务对象是谁**(比如*User进行共享之后就变成了*ptr,然后我们要是想去解析这个内存还需要User这个结构体信息,而这个元信息就是存的这个东西)**

并发编程

并发的核心哲学是:"不要通过共享内存来通信,而要通过通信来共享内存。"

Goroutine:轻量级"线程"------协程

  • **在其他语言(如 Java 或 C++)中,创建一个线程通常需要消耗几 MB 的内存。而在 Go 中,启动一个 Goroutine 只需几 KB。**这意味着你可以在一台普通的笔记本电脑上轻松运行数万个、甚至数十万个并发任务

  • 启动方式极其简单,只需要在函数调用前加一个 go 关键字:go doSomething() // 这个函数会异步执行,不会阻塞主流程

    • 当你调用一个函数时,它是同步 的;当你在它前面加个 go,它就变成了异步
  • **在 Go 中,main 函数运行在"主协程"上。一旦 main 执行结束,程序会立即退出,它不会等待其他后台协程执行完,因此我们需要等待的方式来让 main**是最后结束的协程->想办法阻塞住main协程

    Go 复制代码
    var wg sync.WaitGroup
    
    wg.Add(1) // 登记一个任务
    go func() {
        defer wg.Done() // 任务结束减去 1
        fmt.Println("任务完成")
    }()
    
    wg.Wait() // 阻塞住,直到计数器变回 0

通道 (Channel)

在 Go 里面,我们不希望协程之间通过修改同一个变量来交流(使用互斥锁是Java的设计哲学),而是希望它们像接力赛一样传棒

  • 通道是有类型 的,你必须指定通道里传的是什么(int, string, 还是你的 User 结构体)

    Go 复制代码
    ch := make(chan int) // 创建一个传递整数的通道,channel的关键字是chan来创建的而且需要使用,make函数来进行创建
    
    // 发送数据 (发送者)
    go func() {
        ch <- 42 // 把 42 塞进通道
    }()
    
    // 接收数据 (接收者)
    value := <-ch // 从通道里取出数据
    fmt.Println(value)
  • "阻塞"特性:通道不仅仅是数据的搬运工,它还是一个天然的同步器

    • 发送阻塞:如果通道里没人接,发送方会一直等在那(直到有人取走)

    • 接收阻塞:如果通道里没东西,接收方会一直等在那(直到有人送来)

channel 的核心不是「传数据」,而是「让 goroutine 互相等待」。 很多时候你要的不是管道里那个值,而是「塞/取」这个动作带来的阻塞和唤醒------也就是同步时序。

其实也就是我们常说的生产者消费者模型,变成了Go的一个关键字了。这种特性让你不需要写任何 Sleep,代码会自动在需要的时候"停下来"等对方

  • 通道默认是没有缓冲的,有缓冲的需要单独声明ch := make(chan int, 3)
Go 复制代码
func main() {
    ch := make(chan int)
    go func() {
        fmt.Println("子goroutine: 准备发送")
        ch <- 42   // ① 塞进去,但没人接 → 卡在这
        fmt.Println("子goroutine: 发送完成")  // ④ 交接成功后才打印
    }()

    time.Sleep(time.Second)   // 故意让主goroutine晚点来接
    fmt.Println("主goroutine: 准备接收")
    v := <-ch   // ② 主来接了 → 此刻交接成功
    fmt.Println("主goroutine: 收到", v)  // ③
}

一般我们使用完成一个channel就需要进行关闭,但是有关channel关闭,有四种特殊情况:

因此我们发现是否有东西很难判断到底是零值还是真的没有东西呢,还是使用双返回值的方案:

Java 复制代码
v, ok := <-ch
// ok == true  → 真收到了数据
// ok == false → channel 已关闭,且肚子已空
  • ok 问的是「我这次到底捞着真东西没有?」。false = 没捞着真东西(因为关了且空了),true = 捞着了。你把它理解成「ch 是否关闭」就会方向反。别记成"是否关闭",记成"是否收到了真数据"。

  • 这里如果通道已经关闭了的话那么就会返回对应channel的nil零值,如果chan int的话那就返回是0


Go的数据竞态

Go也是有数据竞态的情况的

Go 复制代码
m := make(map[string]int)
go func() { m["a"] = 1 }()
go func() { m["b"] = 2 }()

map ** 在并发读写下是不安全的。** 只要有一个协程在写,其他协程无论是读还是写,都会触发 fatal error: concurrent map writes

  • 方案 A:Java类型的守旧派 ------ 互斥锁 (Mutex)

    Go 复制代码
    var mu sync.Mutex
    mu.Lock()
    m["a"] = 1
    mu.Unlock()
  • 方案 B:Go 派 ------ 通道 (Channel) 推荐方式

    Go 复制代码
    results := make(chan struct{k string; v int})
    // 协程只管发数据
    go func() { results <- struct{k string; v int}{"a", 1} }()
    // 主协程(或专门的写入协程)负责收
    res := <-results
    m[res.k] = res.v
    • 通道(Channel)其实就是 Go 在语言层面内置的一个"线程安全的阻塞队列" ,它是实现生产者-消费者模式的绝佳工具,而生产消费者模式就是我们避免数据竞态的绝佳的方式

select 语句

select 的语法看起来像 switch,但它的每一个 case 后面必须是一个通道操作

Go 复制代码
select {
case msg1 := <-chan1:
    fmt.Println("收到通道1的消息:", msg1)
case chan2 <- "hello":
    fmt.Println("成功给通道2发送了消息")
default:
    fmt.Println("目前没有任何通道准备好")
}
  • 如果没有 defaultselect 会一直等在那里,直到其中任意一个 case 的通道可以进行读写操作

  • 如果同时有多个通道准备好了,Go 会随机选择一个执行。这保证了每一个通道都有公平的机会被处理,不会因为某个通道太忙而导致其他通道"饿死"

  • 有了 defaultselect 就会变成非阻塞模式。如果没有任何通道就绪,它会直接执行 default 里的逻辑,而不是在那死等

  • 这就是Go的多路复用机制的实现------当前协程可以执行多个任务,但是不进行阻塞------只要有可以执行的就先执行

nil channel:读写都永久卡死,不报错

nil channel 收发不报错,直接永久卡死。有个高级用途:在 select 里把某个 case 的 channel 设成 nil,就能动态关掉那个分支

**如果 ch1 关了你不把它设 nil,那麻烦了------关闭的 channel 是立刻返回零值、不阻塞的,于是 case <-ch1 会疯狂被选中,每次都拿到 (0, false),CPU 空转。**把它设成 nil,正好把这个「关闭后狂转」的分支摘干净。

Go 复制代码
func merge(ch1, ch2 <-chan int) {
    for ch1 != nil || ch2 != nil {   // 只要还有一个没被摘掉,就继续
        select {
        case v, ok := <-ch1:
            if !ok {        // ch1 已关闭且排空
                ch1 = nil   // ★ 关键:把 ch1 设成 nil → 这个 case 从此永远不就绪
                continue    // select 以后会自动跳过这个分支
            }
            fmt.Println("来自 ch1:", v)
        case v, ok := <-ch2:
            if !ok {
                ch2 = nil   // ★ 同理摘掉 ch2
                continue
            }
            fmt.Println("来自 ch2:", v)
        }
    }
    fmt.Println("两个 channel 都关完了,退出")
}

死锁

死锁 A:无缓冲 channel,自己发给自己(没有别的 goroutine 来接)

Go 复制代码
func main() {
    ch := make(chan int)  // 无缓冲
    ch <- 1               // 💥 卡死:主 goroutine 想发,但没有任何人来接
    fmt.Println(<-ch)     // 永远执行不到
}
// fatal error: all goroutines are asleep - deadlock!

死锁 B:缓冲满了,没人取

Go 复制代码
func main() {
    ch := make(chan int, 2)
    ch <- 1   // 缓冲 [1]
    ch <- 2   // 缓冲 [1,2],满了
    ch <- 3   // 💥 卡死:缓冲满,等人来取腾位置,但没有别的 goroutine 会取
}

Context (上下文)

Context 是一个接口(需要引入context包),它最常用的功能可以概括为:

  • WithCancel :手动取消。发出一声令下,所有监听这个 Context 的协程全部停止。

  • WithTimeout ** / ****WithDeadline**:自动取消。比如规定"这个请求最多跑 5 秒",超时自动触发取消信号。

  • WithValue:传递元数据。在协程链路中传递请求 ID、用户信息等

用户点击"搜索机票",后端接收到请求,启动了 3个并发任务:去国航查、去南航查、去东航查。

突然,用户觉得网速慢,点了一下"取消"或者直接关闭了浏览器Context就是为了应对这种情况,用户取消了连接就没有必要继续执行子任务了(尤其是这种没有接收的死任务)

Go 复制代码
package main

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

func main() {
        // 1. 创建一个"2秒后自动过期"的上下文
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()// 在函数结束时释放资源
        // 2. 启动一个干活的协程
        go doHardWork(ctx)
        // 3. 主程序等一会儿,看看结果
        time.Sleep(3 * time.Second)
        fmt.Println("主程序结束")
}

func doHardWork(ctx context.Context) {
        for {
                select {
                case <-time.After(5 * time.Second): 
                        // 假设这个活儿要干 5 秒
                        fmt.Println("任务终于干完了!")
                        return
                case <-ctx.Done():
                        // 重点!这里一直在监听"超时信号"
                        // 一旦 2 秒时间到,这里就会收到信号
                        fmt.Println("【救命】收到指挥部撤退命令,任务太久了,我不干了!")
                        return
                }
        }
}

Go 的并发模型是「随手 go 一个 goroutine」,但 **goroutine 之间没有父子关系、没有「杀掉子线程」的机制。**context不是去「杀」goroutine,而是给所有 goroutine 发一个「请你自己退出」的广播信号,让每个 goroutine 自觉收手。「取消信号广播」本质就是「close 一个 channel」


原理
Go 复制代码
type Context interface {
    Done() <-chan struct{}   // ★ 核心:返回一个 channel
    Err() error              // 取消的原因
    Deadline() (time.Time, bool)
    Value(key any) any       // 携带值
}

Done() 返回的 channel 就是整个机制的命门。平时这个 channel 开着且没数据 → <-ctx.Done() 一直阻塞。一旦取消 / 超时发生,context 内部把这个 channel close 掉。

Go 复制代码
func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():        // 一旦 ctx 被取消,这个 case 立刻就绪
            fmt.Println("收到取消信号,退出:", ctx.Err())
            return                // 自觉收手
        case task := <-taskCh:
            process(task)
        }
    }
}

一旦ctx被取消,那么就是一个立刻返回零值、不阻塞的操作,因此这个关闭的逻辑就可以开始执行了

GMP 模型

GMP 模型(G=goroutine、M=OS线程、P=处理器)

Go Modules(包管理器)

通过两个文件来锁定你的项目依赖:

  • go.mod:定义模块路径和你依赖的库列表

  • go.sum:记录依赖库的校验和,确保安全性,防止你下载的代码被中途掉包

Go的包管理器和Rust的Cargo很类似,只不过没有一个统一管理依赖的网站create.io

  • **go mod init <模块名>****:**初始化项目。模块名通常是你的 GitHub 仓库地址,比如 go mod init ``github.com/user/myproject

  • **go get <包名>****:**下载第三方库。go get -u ``github.com/gin-gonic/gin。**它会自动下载并把版本信息写进 **go.mod

  • go mod tidy****:代码大扫除它会扫描你的代码,自动把没用到的包删掉,把代码里 import 了但还没下载的包补上

Go的目录规范:

Python 复制代码
my-project/    
├── cmd/                # 程序入口(Main 函数所在)
│   └── my-app/         # 每个子目录代表一个编译出的二进制文件
│       └── main.go
├── internal/           # 私有包(不想被别人引用的代码)
│   ├── auth/           # 内部认证逻辑
│   └── db/             # 内部数据库操作
├── pkg/                # 公共库(可以被其他项目引用的代码)
├── api/                # API 定义文件(如 Swagger, Proto 文件)
├── configs/            # 配置文件模板或默认配置
├── deployments/        # 部署相关(Docker, Kubernetes)
├── scripts/            # 脚本文件(编译、工具等)
├── test/               # 额外的外部测试数据和测试脚本
├── go.mod              # 依赖管理
└── go.sum              # 依赖校验

反射

反射赋予了程序在运行时(Runtime)检查、修改自身状态和类型的能力

Go 的反射主要由 reflect 包提供,它围绕两个核心支柱展开:

  1. reflect.Type:它是关于"我是谁"的信息(类型名、字段、方法等)

  2. reflect.Value:它是关于"我值多少"的信息(实际的数据内容、可以对其进行读写)

Go 复制代码
package main

import (
        "fmt"
        "reflect"
)

type User struct {
        Name string `validate:"min:3"` // 这里的标签是反射能读到的核心
        Age  int
}

func ValidateStruct(s interface{}) {
        v := reflect.ValueOf(s)
        t := reflect.TypeOf(s)

        // 如果传进来的是指针,我们需要拿到它指向的具体内容
        if t.Kind() == reflect.Ptr {
                v = v.Elem()
                t = t.Elem()
        }

        // 遍历所有字段
        for i := 0; i < t.NumField(); i++ {
                field := t.Field(i)      // 拿到字段类型信息(包含 Tag)
                value := v.Field(i).String() // 拿到字段实际的值

                tag := field.Tag.Get("validate") // 获取标签内容
                if tag == "min:3" && len(value) < 3 {
                        fmt.Printf("错误:字段 [%s] 的长度太短了!当前值: %s\n", field.Name, value)
                }
        }
}

func main() {
        u := User{Name: "Go", Age: 10}
        ValidateStruct(u) // 这里的 Name 只有 2 位,不符合 min:3
}
  • 反射性能和普通代码性能差距通常在 10 到 100 倍 之间

  • Go 强大的类型检查在反射面前完全失效。如果你写错了字段名,编译器不会报错,程序运行到那一刻直接 panic 崩溃

  • 代码维护起来困难------需要大量的类型断言等语句

Go语言社区的标准就是,除了少数必须使用的基建设施------比如序列化JSON/XML等,基本不要使用反射来构建代码,这也是和Java的最重要的区别**(Go希望所有的细节都是显式的,不用IOC这些黑盒一样的东西,Java的开发速度是很快的,因为有注解这些魔法一样的东西,但是因为所有的都不是显式出来的,因此可能导致很多底层的问题是没有办法轻易修复的)**

Web框架------Gin

路由与基础请求

路由(Routing)决定了哪个 URL 路径由哪段代码来处理。它支持标准的 HTTP 方法(GET, POST, PUT, DELETE)

静态路由、参数路由、通配符路由

  • Gin 的路由分为三类:静态路由参数路由通配符路由

    1. **静态路由:**路径固定,一对一匹配

      Go 复制代码
      r := gin.Default()
      
      r.GET("/ping", func(c *gin.Context) {
          c.JSON(200, gin.H{"message": "pong"})
      })
      
      r.POST("/submit", func(c *gin.Context) {
          c.String(200, "数据已提交")
      })
    2. **参数路由 (Path Parameters)😗*通过 :name 这种语法定义动态片段

      Go 复制代码
      r.GET("/user/:id", func(c *gin.Context) {
          // 使用 c.Param 获取 URL 路径中的变量
          id := c.Param("id") 
          c.String(200, "正在查询用户 ID 为: %s", id)
      })

    如果同时出现静态路由 /user/admin 和参数路由 /user/:id ,访问 /user/admin 时会进入静态路由,因为其优先级更高
    3. 通配符路由 (Catch-all):使用 *filepath。它会匹配该路径下的所有内容,通常用于处理静态文件服务

    Go 复制代码
    r.GET("/assets/*filepath", func(c *gin.Context) {
        path := c.Param("filepath")
        c.String(200, "你正在访问资源路径: %s", path)
    })

路由分组

  • 路由可以进行分组------使用Group函数来进行分组

    Go 复制代码
    func main() {
        r := gin.Default()
        // v1 版本的 API 分组
        **v1 := r.Group("/v1")**
        {
            v1.GET("/login", loginHandler)
            v1.GET("/submit", submitHandler)
        }
        // v2 版本的 API 分组,甚至可以嵌套
        v2 := r.Group("/v2")
        {
            user := v2.Group("/user")
            {
                user.GET("/info", infoHandler) // 实际访问路径: /v2/user/info
            }
        }
        r.Run()
    }

参数处理与数据绑定

获取数据主要分为两种风格:一种是手动提取 (适合简单场景),另一种是自动绑定(最强杀手锏,适合复杂业务)

手动绑定

  • 获取 Query 参数(URL 问号后面的):****?id=100&name=gemini

    • c.Query("name"):获取参数

    • c.DefaultQuery("page", "1"):获取参数,如果没传就给个默认值

  • 获取 Form 表单参数(POST 提交)

    • c.PostForm("username")

自动绑定

ShouldBind它可以自动根据 Content-Type(JSON、XML、YAML、Form)将数据填入你的 Struct 中**(这个比Java清晰太多了)**

Go 复制代码
type LoginRequest struct {
    // tag 里的 json 代表 JSON 字段名,binding:"required" 代表必填
    User     string `json:"user" binding:"required"`
    Password string `json:"password" binding:"required,min=6"`
}

func LoginHandler(c *gin.Context) {
    var login LoginRequest

    // ShouldBindJSON 会自动检查 Header 并解析 Body 里的 JSON
    if err := c.ShouldBindJSON(&login); err != nil {
        // 如果字段缺失或长度不够,err 会有详细信息
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    c.JSON(200, gin.H{"status": "登录成功", "welcome": login.User})
}

自动绑定是依赖于结构体Tag的补充信息的,而Gin主要有三类标签:

参数校验

Gin 内置了 validator 库,你可以直接在标签里写逻辑来进行参数校验,参数之间用逗号进行分隔

  • 必填binding:"required"

  • 范围binding:"min=10,max=20"

  • 格式binding:"email"(必须符合邮箱格式) 或 binding:"url"(必须是有效的 URL 地址)

  • 跨字段校验binding:"eqfield=Password"(比如确认密码必须等于密码字段)

中间件------拦截器+AOP

中间件的固定格式就是一个接受 *gin.Context 的函数

Go 复制代码
// 这是一个最简单的打印日志的中间件
func MyMiddleware(c *gin.Context) {
    fmt.Println("我是中间件:请求进来了,我要先查一下你的身份")
    // 中间件的核心逻辑:c.Next() 代表放行,让后面的业务逻辑跑起来
    c.Next() 
    fmt.Println("我是中间件:业务逻辑跑完了,我最后再收个尾")
}
  • c.Next():暂停当前代码,先去跑后续的业务逻辑,跑完后再回来

  • 这个一般都是用来构建类似Java的Spring里面的AOP(面向切面编程)的东西(比如拦截器等等)

  • 我们使用r.Use(MyMiddleware)这类来进行配置到gin.Engine中去

然后我们需要配置使用逻辑,在哪里用:

  1. 全局拦截

    Go 复制代码
    r := gin.Default()
    r.Use(MyMiddleware) // 只要是发到这个服务器的请求,都会被它拦截
  2. 给特定的分组用(最常用)

    JavaScript 复制代码
    admin := r.Group("/admin")
    admin.Use(MyMiddleware) // 只有 /admin/xxx 的请求会被拦截
    {
        admin.GET("/dashboard", func(c *gin.Context) {
            c.String(200, "这是后台看板")
        })
    }
  3. 给单个接口用

    Go 复制代码
    r.GET("/secret", MyMiddleware, func(c *gin.Context) {
        c.String(200, "这是秘密内容")
    })

GORM

这是Go最常用的持久化ORM框架,和Java里面的MyBatis基于XML的配置不一样,GROM是基于结构体Tag的(又Win了)

Go 复制代码
type User struct {
    // gorm.Model 内部包含了 ID, CreatedAt, UpdatedAt, DeletedAt 
    // 这是 GORM 的"全家桶"配置,推荐带上
    gorm.Model 
    
    Name     string `gorm:"column:user_name;type:varchar(100);unique"`
    Age      int    `gorm:"default:18"`
    Email    string `gorm:"index"` // 自动创建索引
}

gorm:"column:..."** 告诉程序:这个字段在数据库里叫什么、是什么类型、有没有索引**

连接数据库

Go 的数据库驱动通常是"插件化"的。你需要导入 MySQL 驱动**,但代码里直接用 GORM 的接口**

Go 复制代码
// 格式:用户名:密码@tcp(地址:端口)/数据库名?参数1=值1&参数2=值2
dsn := "root:123456@tcp(127.0.0.1:3306)/my_db?charset=utf8mb4&parseTime=True&loc=Local"
  • ?charset=utf8mb4:使用支持表情符号的 UTF-8 编码

  • parseTime=True告诉 GORM 把数据库里的 datetime 字段自动转成 Go 的 time.Time 类型。如果不加这一句,读时间会报错

  • loc=Local:使用本地时区

  • 如果你的密码里有 @:/(比如 pass@word),直接写在 DSN 里会破坏格式导致解析失败。需要对特殊字符进行 URL 编码(比如 @ 变成 %40

Go 复制代码
sqlDB, _ := db.DB()

// 设置最大空闲连接数
sqlDB.SetMaxIdleConns(10)
// 设置最大打开连接数
sqlDB.SetMaxOpenConns(100)
// 设置连接最大存活时间
sqlDB.SetConnMaxLifetime(time.Hour)

拿到 db 对象后,通常要设置一下连接池

增删改查操作

Go 复制代码
// 需要先配置好dsn
// 连接数据库
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})

// 自动迁移(非常爽):它会自动检查你的 Struct,然后在数据库里建好对应的表
db.AutoMigrate(&User{})

AutoMigrate非常好用:

  • 自动创建:如果表不存在,它会根据结构体名(自动转复数)建表

  • 增量更新 :如果你在 User 里新加了一个 Age int 字段,再次运行 AutoMigrate,它会执行 ALTER TABLE users ADD COLUMN age

  • 索引维护 :你在标签里写的 uniqueIndex,它也会帮你创建

  • **不会删除列,不会修改列类型:**一旦类型没有匹配上会报错的,需要进行手动SQL修改维护


  • 增 (Create)

    Go 复制代码
    user := User{Username: "Gemini", Password: "123"}
    result := db.Create(&user) // 传入指针
    // result.Error 可以看有没有报错
    // result.RowsAffected 可以看影响了几行
  • 查 (Query)

    Java 复制代码
    var u User
    // 查询第一条(按 ID 排序)
    db.First(&u) 
    // 根据条件查一条
    db.Where("username = ?", "Gemini").First(&u)
    // 查询全部
    var users []User
    db.Find(&users)
  • 改 (Update)

    Go 复制代码
    // 方式1:更新单个字段
    db.Model(&u).Update("Password", "new_password")
    
    // 方式2:更新多个字段(通过 map 或 结构体)
    db.Model(&u).Updates(User{Username: "NewName", Password: "666"})
  • 删 (Delete)

    Go 复制代码
    db.Delete(&u) 
    // 如果模型里有 DeletedAt(gorm.Model 自带),这只是"软删除",数据还在,只是查不出来了。没有就真删了

相关推荐
七老板的blog2 小时前
当 Spring StateMachine 遇见大模型:构建工业级 AI 写作流水线
java·人工智能·spring
小小编程路2 小时前
Python 还有容器类型互转、进制转换、字符编码转换
开发语言·windows·python
qeen872 小时前
【C++】类与对象之类的默认成员函数(二)
android·c语言·开发语言·c++·笔记·学习
云烟成雨TD2 小时前
Spring AI 1.x 系列【46】MCP Security 模块
java·人工智能·spring
CRMEB系统商城2 小时前
CRMEB多商户系统(Java)v2.3公测版发布
java·开发语言·人工智能·小程序·开源·php
sinat_255487812 小时前
第七部分。介绍MVC(模型-视图-控制器)模式
java·ide·http·tomcat·intellij-idea
动能小子ohhh2 小时前
DocForge平台的设计与开发--文件上传接口的实现
开发语言·人工智能·python·langchain·ocr·fastapi
满天星83035772 小时前
【Qt】信号和槽(二) (自定义信号和槽)
开发语言·数据库·qt
李白的天不白2 小时前
ps -ef | grep java
java