GO语法速通

简介

学习资料:

  1. https://golang3.eddycjy.com/posts/func-overloading-defalut/
  2. 官方练习网站:https://go.dev/tour/list

首先先简单介绍下go语言的基本特性

  1. 强类型,支持类型推断
  2. 编程范式:Go不是传统意义上的OOP语言,但包含了一些OOP特性。没有类的概念,只有结构体和接口。不支持继承,通过组合实现代码复用
  3. 带GC,可直接编译成机器码
  4. 早期版本不支持泛型, 从 1.18 版本才开始支持泛型
  5. 内置并发模型,通过goroutines实现轻量级的线程,允许高并发的处理

常用语法

变量与类型

使用var关键字声明变量:var variable_name type = expression

  1. 类型可以不要,变量的类型支持由表达式中的初始化值进行推断
  2. =可以不写,只定义变量,不进行初始化
package main

import "fmt"

func main() {

    // 变量声明和初始化不使用表达式
    var v1 int
    var v2 string
    var v3 float64

    // 显示默认值
    fmt.Printf("myvariable1的值是 : %d\n", myvariable1)
    fmt.Printf("myvariable2的值是 : %d\n", myvariable2)
    fmt.Printf("myvariable3的值是 : %d\n", myvariable3)
}

不初始化则该变量的类型为默认值,数字为零,布尔值为false,字符串为'',接口和引用类型为nil。

如果使用类型,则可以在单个声明中声明相同类型的多个变量。

var a, b, c int = 1, 2, 3

如果删除类型,则可以在单个声明中声明不同类型的多个变量。变量的类型由初始化值确定。

var a, b, c= 2, "GFG", 67.56

获取变量的类型

var a = 20.0
fmt.Printf("the type of a is %T\n", a)  // the type of a is float64

未使用的变量

声明变量后未使用,会出现编译错误

var a = 10.0

短变量声明

使用短变量声明来声明在函数中声明和初始化的局部变量。语法:variable_name:= expression

这种方式简洁、灵活,大多数局部变量都是使用短变量声明来声明和初始化的。

var用于声明那些需要与初始值设定项表达式不同的显式类型的局部变量,或用于其值稍后分配且初始值不重要的那些变量。

这种声明变量的方式不能使用var关键字,同时也不能添加类型声明

声明多个变量

可以同时声明多个变量:a, b, c := 1, 2, 3,多个变量可以有不同类型,可以自动推断

简短的变量声明仅当对于已在同一语法块中声明的那些变量起作用时,才像赋值一样。在外部块中声明的变量将被忽略。如下面的示例所示,这两个变量中至少有一个是新变量。

:= 只能声明新的变量,不能重新声明一个已经声明过的变量。下面这种写法会出现编译错误

a, b := 39, 45
b := 100 // Error! No new variables on the left side of ':='

但使用 :=为多个变量赋值时,如果引入了至少一个新的变量,编译是可以通过的

a, b:= 39, 45
c, b:= 45, 100  // b的值从45更改为100

注意这个时候,并没有重新声明b变量,只是为之前声明的b变量在当前这个作用域内赋值

再看下面这个例子,嵌套作用域情况下可以有相同名称的变量,会出现命名遮蔽(shadowing)的情况,java中是没有这个特性的。使用 var 声明的变量和 := 的变量现象不同

var a = 1
b := 45
fmt.Printf("%d\n", a) // a = 1
fmt.Printf("%d\n", b) // b = 45
{
	fmt.Printf("%d\n", a) // a = 1
	fmt.Printf("%d\n", b) // b = 45
	a = 10
	b := 100
		fmt.Printf("%d\n", a) // a = 10
	fmt.Printf("%d\n", b) // b = 100
}
fmt.Printf("%d\n", a)  // a = 10, a 已经被修改
fmt.Printf("%d\n", b)  // b = 45

类型别名

从 Go1.9 开始引入了类型别名,使用type关键字

type intalias = int

Go 中可以为任意类型定义别名,比如数组、结构体、指针、函数、接口、Slice、Map、Channel 等,包括为自定义类型定义别名。

type F = func()
type I = interface{}

此外,还可以为其他包中的类型定义别名,比如为标准库类型定义别名:

type MyReader = bufio.Reader

注意事项
1.别名和原类型是一样的,因此 switch-type 结构中,不能存在两个 case,一个是原类型,一个是别名;

2.类型别名不能循环定义,比如以下是不允许的:

type T = struct {
	next *T1
}

type T1 = T

3.别名和原类型是一样的,因此共享同样的方法集,不论这个方法是定义在原类型还是别名上;
4.别名的导出性可以和原类型不一样;
5.不能为别的包的类型通过定义别名来增加方法。以下行为是不允许的:

type MyReader = bufio.Reader
func (MyReader) AliasMethod() {
	fmt.Println("This is alias method")
}

会导致编译错误:bufio.Reader
6.Go 内置类型中,内置别名包括:rune 是 int32 的别名,byte 是 uint8 的别名
在 Go1.9 之前,rune 和 byte 的别名性质就存在,是编译器负责处理的。只是 Go1.9 之后,别名可以用于其他类型了。

作用域

package main
import "fmt"

// 全局变量声明
var v1 int = 100

func main() {
    // 主函数内部的局部变量
    var v2 int = 200

    // 显示全局变量
    fmt.Printf("全局变量 myvariable1 的值是 : %d\n", v1)

    // 显示局部变量
    fmt.Printf("局部变量 myvariable2 的值是 : %d\n", v2)
}

数据类型

Golang是静态类型、编译型语言

基本数据类型

  • 布尔型:true/false。
  • 整型:包括int8、int16、int32、int64、uint8(别名byte)、uint16、uint32、uint64、int、uint等。其中int和uint的大小是依赖于运行它的系统架构的(32位或64位)
  • Floating-Point Types:包括float32和float64两种类型。float64是Go的默认浮点类型。

byte是内置类型uint8的一个别名

nil

在Go语言中,用 nil 表示指针、通道(channel)、函数、接口、映射(map)、切片(slice)或数组的零值。当声明了这些类型的变量但还没有给它们分配任何值时,这些变量的默认值就是 nil。然而,需要注意的是,基本数据类型(如整数、浮点数、布尔值和字符串)的零值并不是 nil,而是该类型的零值(如整数的零值是 0,字符串的零值是空字符串 "" 等)。

字符串

Go的字符串是不可变的字节序列,使用双引号

Go支持两种风格的字符串字面量表示形式:双引号风格(解释型字面表示)和反引号风格(直白字面表示)。

字符串类型的零值为空字符串,空字符串在字面上可以用""或者``来表示

var s4 = "string4"
var s5 = `string5`

可以用运算符+和+=来拼接字符串。

var s4 = "hello" + `world`

字符串类型都是可比较类型。同一个字符串类型的值可以用==和!=比较运算符来比较。 并且和整数/浮点数一样,同一个字符串类型的值也可以用>、<、>=和<=比较运算符来比较。 当比较两个字符串值的时候,它们的底层字节将逐一进行比较。如果一个字符串是另一个字符串的前缀,并且另一个字符串较长,则另一个字符串为两者中的较大者。

package main

import "fmt"

func main() {
	const World = "world"
	var hello = "hello"

	// 衔接字符串。
	var helloWorld = hello + " " + World
	helloWorld += "!"
	fmt.Println(helloWorld) // hello world!

	// 比较字符串。
	fmt.Println(hello == "hello")   // true
	fmt.Println(hello > helloWorld) // false
}

字符串值和布尔以及各种数值类型的值可以被用做常量。

strings模块提供

  1. len(s): builtin模块中提供的方法,返回字符串s的字节长度(注意是字节长度,不是字符,对于UTF-8编码的字符串比如中文,可能不等于字符数)。
  2. +或 fmt.Sprintf: 用于字符串拼接。+ 是直接的字符串连接,而fmt.Sprintf可以用于格式化字符串,包括将非字符串类型转换为字符串并拼接。
  3. strings.ToUpper(s), strings.ToLower(s): 将字符串s中的所有字符转换为大写或小写。
  4. strings.TrimSpace(s): 去除字符串s首尾的空白字符(包括空格、换行符\n、制表符\t等)。
  5. strings.Trim(s, cutset): 去除字符串s首尾的cutset指定的字符集合中的字符。
  6. strings.TrimLeft(s, cutset), strings.TrimRight(s, cutset): 分别去除字符串s左侧的或右侧的cutset中的字符。
  7. strings.Replace(s, old, new, n): 将字符串s中的前n个old子串替换为new,如果n为-1,则替换所有。
  8. strings.Split(s, sep): 使用sep作为分隔符将字符串s分割成切片,但不包括分隔符本身。
  9. strings.Join(a []string, sep string): 使用sep将字符串切片a中的元素连接成一个字符串。
  10. strings.Contains(s, substr): 判断字符串s是否包含子串substr。
  11. strings.HasPrefix(s, prefix), strings.HasSuffix(s, suffix): 判断字符串s是否以prefix开头或以suffix结尾。
  12. strings.Count(s, substr): 返回字符串s中子串substr非重叠出现的次数。
  13. strings.Index(s, substr), strings.LastIndex(s, substr): 返回子串substr在s中第一次或最后一次出现的位置索引,如果未找到则返回-1。

遍历字符串

可以使用for range循环遍历字符串中的每个字符

for index, runeValue := range "hello" {  
    fmt.Println(index, string(runeValue))  
}

对于UTF-8编码的字符串,实际上是rune类型

字符串不可变

Go语言的字符串是不可变的,一旦创建了字符串,就不能更改字符串中的字符。上述方法都是基于创建新的字符串来“修改”原有字符串的

Complex

包括complex64和complex128两种类型。
用于表示复数,例如c := complex(1, 2)表示1+2i。

字符类型(Rune Type)
字符类型表示为rune,是int32的别名。
用于表示Unicode字符。

复合数据类型

数组

相同的类型、固定长度的元素序列

// 空数组
var arr1 = [5]int{}
var arr2 [5]int = [5]int{1, 2, 3, 4, 5}  
// 或者更简洁的写法  
arr3 := [5]int{1, 2, 3, 4, 5}
// 如果数组的元素类型可以从初始化中推断出来,你还可以省略类型声明:
arr4 := [...]int{1, 2, 3, 4, 5} // 编译器会自动计算长度

由于数组的长度是类型的一部分,因此不同长度的相同类型数组被视为完全不同的类型。

Go语言中数组是值传递的。将一个数组作为参数传递给一个函数时,实际上传递的是数组的副本,而不是数组本身。函数内部对数组的任何修改都不会影响到原始数组。

对于大型数组,这种传递方式可能会导致性能问题。在需要动态长度或性能敏感的场合,建议使用切片。

package main

import "fmt"

func main() {
	var arr [5]int = [5]int{1, 2, 3, 4, 5}
	fmt.Println("len(arr) =", len(arr), "arr[2] =", arr[2])
	modifyArray(arr) // 传递数组给函数
	fmt.Println(arr) // 注意:arr的内容不会改变,因为数组是值传递
}

func modifyArray(a [5]int) {
	a[0] = 100 // 修改数组的第一个元素
}

切片 Slice

go的切片类似于java的ArrayList,切片是引用类型

切片的类型为[]int,[]int32等等

package main

import "fmt"

func main() {
	var s1 []int               // 声明了一个切片s,但s是nil,没有分配内存
	s2 := []int{1, 2, 3}       // 声明并初始化切片s
	var s3 = make([]int, 0, 5) // 使用make初始化s,长度为0,容量为5

	// 可以像访问数组元素一样访问切片元素,使用索引
	fmt.Printf("s1:%v\ns2:%v\ns3:%v\n", s1, s2, s3)

	fmt.Println(len(s3)) // 输出切片的长度
	fmt.Println(cap(s3)) // 输出切片的容量

	// 容量不够时会进行扩容
	s3 = append(s3, 4) // 向切片s追加元素4

	s4 := s3[1:3] // 创建一个新的切片s2,包含s中索引为1和2的元素

	fmt.Println(s4)

	// 切片是引用类型,可以使用copy()函数来复制切片的内容到另一个切片中
	var s5 []int = make([]int, len(s4))
	copy(s4, s5) // 将s的内容复制到s3中
}

当切片引用的底层数组被GC时,切片也会变得不可用。
切片是引用类型,所以在函数间传递切片时,传递的是切片的引用,而不是切片的副本。
使用append()向切片追加元素时,如果追加导致容量不足,Go会分配一个新的底层数组,并更新切片的引用。因此,如果你将切片作为参数传递给函数,并在函数内部使用append()修改了切片,那么原始切片也会受到影响

Map

go内置map结构,无序

map的键可以是任何可比较的类型,如整数、浮点数、字符串或自定义类型等,但键必须是唯一的。值可以是任意类型。

var m map[string]int  // 声明map: map[KeyType]ValueType

这样声明的map是nil的,不能直接使用。你需要在声明后使用make函数来分配内存,或者使用map字面量来初始化它。

m = make(map[string]int) // 使用make函数初始化  
  
// 或者使用map字面量初始化  
m := map[string]int{  
    "one":   1,  
    "two":   2,  
    "three": 3,  
}

下面是map的一些常见操作

package main

import "fmt"

func main() {
	// 使用map字面量初始化
	m := map[string]int{
		"one":   1,
		"two":   2,
		"three": 3,
	}
	value := m["one"] // 返回1
	// 检查键是否存在
	if value, ok := m["four"]; ok {
		fmt.Println("Found:", value)
	} else {
		fmt.Println("Not found")
	}
	fmt.Println(value)

	// 遍历 map
	for key, value := range m {  
		fmt.Println(key, value)  
	}
}

map的键必须是可比较的
map的遍历顺序是不确定的,且每次遍历的顺序都可能不同。

map线程不安全
键不存在将返回该值的零值,并且没有错误提示。如果需要检查键是否存在,请使用上面提到的 if value, ok := m[key]; ok的语法。

指针

Go 的指针做了极简封装,没有 C/C++ 指针的复杂操作(指针运算 / 偏移)

  • 不支持指针运算 :不能对指针做 +/- 偏移(比如 p++),避免越界访问内存,百分百安全;
  • 不支持指针强制类型转换 :避免非法内存访问。

没有野指针、悬空指针的风险

Go 中变量的内存地址格式:0xc0000100a8 这类十六进制字符串

和C++中一样:

  1. & 取地址符 :获取一个「普通变量」的 内存地址
  2. * 解引用符:通过一个「指针变量存储的内存地址」, 找到并访问内存地址对应的原始变量 ,拿到原始变量的值。

& :变量 → 地址 (把变量变成它的地址),* :地址 → 变量 (把地址变回它的变量),二者是 互逆操作*&a 等价于 a

package main
import "fmt"

func main() {
	// 1. 定义一个普通变量
	var num int = 100
	fmt.Printf("普通变量 num 的值:%d\n", num)      // 输出:100
	fmt.Printf("普通变量 num 的内存地址:%p\n", &num) // 输出:0xc0000100a8 (地址随机,正常)

	// 2. 定义一个「指针变量」,存储num的内存地址
	// 语法:var 指针变量名 *变量类型 = &普通变量名
	var p *int = &num
	fmt.Printf("指针变量 p 的值:%p\n", p)        // 输出:0xc0000100a8 (就是num的地址)
	fmt.Printf("指针变量 p 自己的内存地址:%p\n", &p) // 输出:0xc00000e028 (指针也是变量,也有自己的地址)

	// 3. 解引用:通过指针变量p,获取原始变量num的值
	fmt.Printf("通过指针p获取num的值:%d\n", *p)  // 输出:100

	// 4. 核心能力:通过指针,修改原始变量的值 ✅
	*p = 200
	fmt.Printf("修改后,num的值:%d\n", num)      // 输出:200 (原始变量被修改了!)
	fmt.Printf("修改后,*p的值:%d\n", *p)        // 输出:200 (指针指向的值同步变化)
}

指针的零值

和普通变量一样,声明了指针变量但未赋值时,指针变量的 零值是 nil (空指针),表示这个指针变量 没有指向任何内存地址

package main
import "fmt"

func main() {
	var p *int // 声明指针变量,未赋值
	fmt.Println(p) // 输出:`<nil>` (空指针)
	// fmt.Println(*p) // ❌ 报错:runtime error: invalid memory address or nil pointer dereference
}

不能对 nil 空指针执行解引用 *p

new 关键字:创建指针的快捷方式,指针变量名 := new(数据类型)

语法作用 :new(数据类型) 会做两件事:

  1. 在内存中开辟一块对应类型的空间,赋值为该类型的「零值」;
  2. 返回这块内存的 地址(指针)

引用

赋值 / 传参时,会 拷贝数据的「内存地址」 ,拷贝后的变量和原始变量 指向同一块内存地址 ,修改拷贝会直接影响原始变量。

Go 中所有的引用类型slice(切片)、map(映射)、channel(通道)、func(函数)

引用类型本身就是「地址的封装」,不需要手动传指针 ,传参时直接传变量名即可修改原始数据!

package main
import "fmt"

func main() {
	// 切片是引用类型
	slice := []int{1,2,3}
	modifySlice(slice)
	fmt.Println(slice) // 输出:[99 2 3] ✅ 原始切片被修改
}

func modifySlice(s []int) {
	s[0] =99
}

Struct

go的结构体类似C和C++中的结构体

type Person struct {  
    Name string  
    Age  int  
    Email string  
}

创建结构体

var person1 Person  
person1.Name = "Alice"  
person1.Age = 30  
person1.Email = "alice@example.com"  
  
// 或者使用结构体字面量直接初始化  
person2 := Person {  
    Name: "Bob",  
    Age:  25,  
    Email: "bob@example.com",  // 最后一个字段也需要加逗号
    // Need a trailing comma before a newline in the composite literal 
}

// 如果字段的顺序与结构体中声明的顺序一致,可以省略字段名  
person3 := Person{"Charlie", 35, "charlie@example.com"}  

// 如果字段多,而只想设置部分字段,可以使用命名字段进行初始化  
person4 := Person{  
    Name: "David",  
    Email: "david@example.com", 
    // 注意这里没有设置Age字段,它将使用零值(对于int是0)  
}

结构体方法

Go语言的结构体不支持继承和方法重载,但可以有方法。方法是附加到类型上的函数,它们具有特殊的接收者参数,该参数在函数体内部用于访问调用它的实例的字段和方法。

type Person struct {
	Name  string
	Age   int
	Email string
}

// value receiver
func (p Person) Greet() {  
    fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}  
  
// 或者为指针接收者(pointer receiver)定义方法,这样可以直接修改接收者的字段  
func (p *Person) SetAge(age int) {  
    p.Age = age  
}

这两种形式的方法可以同时定义,但是会有警告
Struct Person has methods on both value and pointer receivers. Such usage is not recommended by the Go Documentation.

value receiver 和 pointer receiver

https://go.dev/tour/methods/8

这两种方式的区别是:

1. 值接收者 :方法内部拿到的是结构体的「拷贝」,修改字段值 不会影响原结构体

2. 指针接收者 :方法内部拿到的是结构体的「内存地址」,修改字段值 会直接影响原结构体

开发原则: 优先使用指针接收者 ,节省内存拷贝,且支持修改原结构体的字段。

无论结构体是值类型还是指针类型,调用方法时都可以直接用 .,Go 会自动做转换,无需手动取地址 / 解引用。

结构体是 Go 的 值类型

  • 结构体变量赋值时,会 拷贝整个结构体的所有字段 ,两个变量互不影响;
  • 函数参数传递结构体时,默认是 值传递 ,同样会拷贝整个结构体,效率低;
  • 解决方案: 传递结构体指针 ,只拷贝内存地址,效率极高,且能修改原结构体。

结构体标签

结构体标签:写在 Go 结构体字段后面、反引号 ` 包裹的字符串,属于附在字段上的元数据注释。格式如下:

type 结构体名 struct {
    字段名 类型 `标签内容`
}

常见例子:写web接口时会这样定义给前端的dto:json:"id" 就是结构体标签。

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Age  int    `json:"age,omitempty"`
}

写orm代码时会像下面这样写

type User struct {
	ID uint `gorm:"primaryKey;autoIncrement;comment:用户ID" json:"id"`
	Name string `gorm:"type:varchar(50);not null;comment:用户名" json:"name"`
	Age  int    `gorm:"type:int;default:0;comment:年龄" json:"age,omitempty"`
}

不同的标签是可以共用的,例如:

type User struct {
	ID       int    `gorm:"primaryKey;autoIncrement" db:"id"`
	UserName string `gorm:"column:user_name" db:"user_name"`
}

标签本身Go 语法不直接执行,不会影响程序逻辑;
作用是给反射(reflect) 提供元数据;
第三方库 / 标准库通过反射读取标签,实现序列化、ORM 映射、参数校验等功能。

这个语法和java比就有点繁琐了,这个标签就是个字符串标签,具体的语法就要看框架怎么解析了,而java中是单独的一个注解语法

多种标签写到一个字符串里就比较难看了,对比java的,虽然代码相对多了点,但是阅读起来更好

public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
	@JsonProperty("id")
    private Integer id;

    @Column(name = "name")
	@JsonProperty("name")
    private String name;

    @Column(name = "age")
	@JsonProperty("age")
    private Integer age;
}

另外一点,go 这个标签是纯字符串,编译器不校验,写错了要在运行时才能知道

type User struct {
	// 你故意写错 json → 编译完全通过!只有运行时不生效
	Name string `jsn:"name"` // 应该是 json,写成 jsn → 不报错
	Age  int    `json:age`   // 少引号 → 编译依然通过!
}

Interface

接口中的方法没有函数体,只有方法名、参数列表和返回类型

type Shape interface {  
    Area() float64
    Perimeter() float64  
}

任何实现了这两个方法的类型都可以被视为实现了Shape接口,而不需要用类似java的implements显式声明,因此是隐式实现,例如:

type Circle struct {  
    radius float64  
}  
  
// Circle类型实现了Shape接口的所有方法  
func (c Circle) Area() float64 {  
    return math.Pi * c.radius * c.radius  
}  
  
func (c Circle) Perimeter() float64 {  
    return 2 * math.Pi * c.radius  
}  
  
// Rectangle类型也实现了Shape接口  
type Rectangle struct {  
    width, height float64  
}  
  
func (r Rectangle) Area() float64 {  
    return r.width * r.height  
}  
  
func (r Rectangle) Perimeter() float64 {  
    return 2*r.width + 2*r.height  
}

接口可用于实现多态

func measure(s Shape) {  
    fmt.Println("Area:", s.Area())  
    fmt.Println("Perimeter:", s.Perimeter())  
}  
  
func main() {  
    c := Circle{radius: 5}  
    r := Rectangle{width: 10, height: 5}  
  
    measure(c)  
    measure(r)  
}

空接口

Go语言还有一个特殊的接口,即空接口(interface{})。空接口不包含任何方法,因此任何类型都实现了空接口。空接口通常用于需要存储任意类型值的场景,如fmt.Println函数和interface{}类型的切片。

var any interface{} = 42  
var str interface{} = "foo"  
var list interface{} = []int{1, 2, 3}

类型转换

Golang即使数据类型兼容,它也不支持自动类型转换或隐式类型转换。原因是Golang的强类型系统不允许这样做。对于类型转换,必须执行显式转换。

var nhooo1 int = 845

// 显式类型转换
var nhooo2 float64 = float64(nhooo1)
var nhooo3 int64 = int64(nhooo1)
var nhooo4 uint = uint(nhooo1)

由于Golang具有强大的类型系统,因此不允许在表达式中混合使用数字类型(例如加,减,乘,除等),并且不允许在两个混合类型之间执行赋值类型。

var a int64 = 875

// 它会在编译时抛出错误给我们, 因为正在执行混合类型,例如把int64作为int类型
var b int = a

var c int = 100
//它抛出编译时错误
//这是无效操作
//因为类型是混合的 int64 和 int 相加
var d = a + c

泛型

Go 1.18+ 才正式支持泛型

  1. 泛型支持联合类型

泛型函数

func AddInt(a, b int) int { return a + b }
func AddFloat(a, b float64) float64 { return a + b }

// 类型参数 [T]
// 类型约束 T int | string
// 实际类型 int / string 等
func Add[T int | float64](a, b T) T {
    return a + b
}

泛型结构体

type Result[T any] struct {
    Code int
    Msg  string
    Data T    // 泛型字段
}

// 返回用户数据
res1 := Result[User]{Code: 200, Data: User{Name: "张三"}}

// 返回字符串
res2 := Result[string]{Code: 200, Data: "成功"}

泛型接口

type Number interface {
    int | int8 | int16 | int32 | int64 |
    float32 | float64
}

// 所有数字都能用
func Sum[T Number](nums ...T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

关键字 any

any 是 interface{} 的别名,表示:任意类型

func Print[T any](v T) {
    fmt.Println(v)
}

Go 泛型的限制

  1. 不能用 == 之外的运算符,除非约束里明确类型
  2. 不能用泛型创建实例
  3. 方法不能额外定义类型参数(只能用结构体的)
  4. 不能用于 map 的 key 以外的自动推导

Go 没有 Java 那种 “上界 / 下界(extends/super)” 语法。也就没有 PECS 法则,没有通配符 ?。用类型约束(interface 里写类型集合)实现了类似 “上界” 的效果:限制 T 只能是某些类型 / 实现某些方法。

枚举 iota

Go 里没有 enum 关键字,iota 不是枚举类型,也不是语法糖,它是编译期确定的 const 块内行号计数器——从 0 开始,每遇到一个新 const 行(非空、非注释、有常量声明),就自动加 1。

iota 不是枚举类型
iota 不是语法糖
iota 是:编译期常量块内的行号计数器
从 0 开始
每一行有效常量声明 +1
只在当前 const 块内生效

const (
    A = iota  // 0 → 第1行
    B         // 1 → 第2行(自动继承 iota)
    C         // 2 → 第3行
    D         // 3
)

本质上 iota = 当前 const 块里的行号(从 0 开始)
不写表达式,自动用上一行表达式(也就是 iota)

go 项目中定义枚举一般的做法是:

package main

type Status int

const (
	StatusNormal  Status = iota // 0
	StatusDisable               // 1
	StatusDeleted               // 2
)

func test(i Status) string {
	if i == StatusDeleted {
		return "normal"
	} else if i == StatusNormal {
		return "disabled"
	} else {
		return "deleted"
	}
}

注意:这不是类型安全,不是真正枚举,只是int 常量别名

strconv.Itoa(i int) string : 把 int 整数转成 十进制字符串

strconv.Itoa(int(StatusNormal))  // "0"
strconv.Itoa(int(StatusDisable))  // "1"
strconv.Itoa(int(StatusDeleted))  // "2"

流程控制

条件判断

if条件表达式不需要括号

if 条件表达式 {  
    // 条件为真时执行的代码  
} else if 另一个条件表达式 {  
    // 另一个条件为真时执行的代码  
} else {  
    // 所有条件都不为真时执行的代码  
}

Go的 switch 更灵活,它不需要 break 语句来阻止自动落入下一个 case,并且它允许 case 后面跟随一个表达式而不是仅一个常量。

switch 表达式 {  
	case 值1:  
	    // 当表达式等于值1时执行的代码  
	case 值2:  
	    // 当表达式等于值2时执行的代码  
	default:  
	    // 当表达式不等于任何值时执行的代码  
}  
  
// 或者不带表达式的switch,相当于switch true  
switch {  
	case 条件1:  
	    // 条件1为真时执行的代码
	case 条件2:  
	    // 条件2为真时执行的代码
	default:  
	    // 所有条件都不为真时执行的代码
}

循环

go中没有do-while,while-do,while(true)等

// 经典的for循环  
for 初始化语句; 条件表达式; 后置语句 {  
    // 循环体  
}  
  
// 类似于while循环  
for 条件表达式 {  
    // 循环体  
}  
  
// 无限循环  
for {  
    // 循环体  
}

range 关键字用于遍历数组、切片、字符串、映射或通道的元素。它经常与 for 循环一起使用。

for index, value := range 集合 {  
    // 使用index和value  
}

continute和break和其它语言一样

跳转

goto 语句用于跳转到程序中的另一个标签处。尽管 goto 在某些情况下很有用(如跳出多层嵌套循环),但通常建议谨慎使用,因为它可能会使代码难以理解和维护。

函数

Go 语言中,所有函数的参数传递,全部都是「值传递」 —— 调用函数时,会把实参的拷贝传递给形参,形参和实参是 两个独立的变量 ,内存地址完全不同。

函数重载

同名函数具有不同的参数列表
编译错误:'Sum' redeclared in this package

func Sum(i int) int {
	return i
}

func Sum(i int16) int16 {
	return i
}
  1. 简化编译器实现:函数重载需要编译器执行大量解析和查找工作,这会增加编译器复杂性和性能开销
  2. 提高代码可读性:每个函数都有唯一的名称,这使得代码更容易阅读和理解,可以明确地知道哪个函数被调用

具名参数 (不支持)

参数默认值 (不支持)

可变参数

Go支持可变参数。在Go中,这是通过在一个参数类型前加上省略号(...)来实现的。这样,该参数就可以接受零个或多个指定类型的参数。

func Sum(nums ...int) int {  
    total := 0  
    for _, num := range nums {  
        total += num  
    }  
    return total  
}  

和java一样,可变参数只能在最后

// Can only use '...' as the final argument in the list
func Sum(nums ...int, items ...int8) int {
	total := 0
	for _, num := range nums {
		total += num
	}
	return total
}

嵌套/继承

golang 不支持继承,推崇通过组合方式实现代码复用
Golang 中的嵌套主要指结构体(Struct)嵌套和接口(Interface)嵌套,用于实现组合和代码复用,替代继承。结构体嵌套通过在一个结构体中包含另一个结构体(具名或匿名)实现,支持字段和方法自动提升;接口嵌套将一个或多个接口嵌入新接口,合并其方法集合。

结构体嵌套 (组合):
具名嵌套: type A struct { b B },访问需 a.b.Field。
匿名字段嵌套 (组合): type A struct { B },内部类型 B 的字段和方法会直接提升到 A 中,访问可直接使用 a.Field。
初始化: 使用 &A{B: B{...}} 或匿名结构体直接嵌套初始化。
接口嵌套:
type ReaderWriter interface { Reader; Writer },实现 ReaderWriter 接口必须同时实现 Reader 和 Writer 的所有方法。
注意事项: 避免出现环形嵌套。如果匿名嵌套的两个结构体包含同名成员,会出现冲突,需要明确显式访问。

type Engine struct {
    Power int
}

type Car struct {
    Engine // 匿名字段,实现嵌套
    Name   string
}

func main() {
    c := Car{
        Engine: Engine{Power: 100},
        Name:   "Tesla",
    }
    // 直接访问内嵌结构体的字段
    println(c.Power) // 输出 100
}

异常处理

Go没有专门的错误类型关键字,但通常使用error接口来表示错误。

错误情况分两种:

  1. error:可预见、可处理的问题(文件不存在、参数错误、网络超时),是正常业务逻辑的一部分
  2. panic:不可预见、致命的问题(数组越界、空指针、程序逻辑 BUG),会终止程序执行

error

error 是 Go 内置接口,函数主动返回错误,调用方需要显式处理,这是 error 的约定,并非强制

type error interface {
	Error() string
}

函数如果要声明异常,需要将error指定为返回值,调用通过判断函数返回的 error 是否为 nil 来决定是否有异常情况

package main

import (
	"errors"
	"fmt"
)

// 定义一个返回 error 的函数
func divide(a, b int) (int, error) {
	if b == 0 {
		// 返回错误:errors.New 创建简单错误
		return 0, errors.New("除数不能为 0")
	}
	return a / b, nil // 无错误返回 nil
}

func main() {
	res, err := divide(10, 0)
	// 必须显式判断错误(Go 强制风格)
	if err != nil {
		fmt.Println("错误:", err)
		return
	}
	fmt.Println("结果:", res)
}

格式化错误文本

错误信息可以格式化显示

return 0, fmt.Errorf("除数不能为 0,输入值:%d", b)

自定义异常

异常可以作为返回值,那么可以定义自己的Error类型实现自定义异常,这种是 web 后端常见的使用场景,例如

// 自定义错误结构体
type MyError struct {
	Code int
	Msg  string
}

// 实现 error 接口(必须实现 Error() string 方法)
func (e *MyError) Error() string {
	return fmt.Sprintf("错误码:%d,信息:%s", e.Code, e.Msg)
}

func test() error {
	return &MyError{Code: 400, Msg: "参数非法"}
}

error 的问题

go 中这种异常设计写法有个问题,就是代码里全是 if err != nil 这种对 error 的判断,这也是go经常被诟病的一点,例如

f, err := os.Open(...)
if err != nil {
    return err
}

d, err := io.ReadAll(f)
if err != nil {
    return err
}

err = f.Close()
if err != nil {
    return err
}

更复杂的还有分支嵌套的场景。比如一个函数里既要做参数校验,又要读取文件,还要根据业务条件继续处理数据,如果按照“成功逻辑包在 if 里面”的方式写,代码会越缩越深:

func handle(path string, enable bool) error {
    if path != "" {
        f, err := os.Open(path)
        if err == nil {
            defer f.Close()

            data, err := io.ReadAll(f)
            if err == nil {
                if enable {
                    err = process(data)
                    if err != nil {
                        return err
                    }
                }
            } else {
                return err
            }
        } else {
            return err
        }
    } else {
        return errors.New("path is empty")
    }

    return nil
}

这种写法的问题是主流程被多层 if 包住,真正重要的业务逻辑藏在缩进最深的地方,可读性会明显变差。Go 里更常见的写法是“遇到错误立即返回”,也就是卫语句(guard clause):

func handle(path string, enable bool) error {
    if path == "" {
        return errors.New("path is empty")
    }

    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()

    data, err := io.ReadAll(f)
    if err != nil {
        return err
    }

    if !enable {
        return nil
    }

    if err := process(data); err != nil {
        return err
    }

    return nil
}

可以看到,Go 的错误处理虽然会产生很多 if err != nil,但推荐的风格是把异常分支提前处理掉,让正常流程尽量保持在同一层缩进里。

另外一个问题是错误没有调用栈,Go 原生 error 不携带堆栈信息。出现 error 时通常只能看到一段错误文本,不一定知道:哪个函数产生的、在哪一层业务里失败的、调用链路是什么。

例如底层函数直接返回 os.Open 的错误:

func loadConfig(path string) error {
    _, err := os.Open(path)
    if err != nil {
        return err
    }

    return nil
}

上层拿到的错误可能只是:

open ./config.yaml: no such file or directory

这条错误能说明文件不存在,但看不出这是“加载配置”时失败,还是“读取用户上传文件”时失败。项目大了之后,同一个底层错误可能从很多地方返回,排查时就会缺少业务上下文。

Go 里常见的做法是在向上返回错误时补充上下文,并使用 %w 包装原始错误:

func loadConfig(path string) error {
    _, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("load config %s failed: %w", path, err)
    }

    return nil
}

func startApp() error {
    if err := loadConfig("./config.yaml"); err != nil {
        return fmt.Errorf("start app failed: %w", err)
    }

    return nil
}

这样最终错误会变成类似:

start app failed: load config ./config.yaml failed: open ./config.yaml: no such file or directory

这不是传统意义上的堆栈,但它形成了一条“错误链”,能从错误文本里看出业务调用路径。同时因为用了 %w,原始错误没有丢失,后续还可以用 errors.Iserrors.As 判断底层错误类型:

if errors.Is(err, os.ErrNotExist) {
    fmt.Println("配置文件不存在")
}

如果确实需要完整调用栈,可以在创建错误时额外记录堆栈,或者在日志中结合 debug.Stack()、第三方错误库、统一日志框架来处理。但日常业务代码里,更推荐先把每一层错误上下文写清楚,不要只返回一个孤零零的 err

如何让异常展示调用栈

如果程序直接 panic,Go 运行时默认会在程序崩溃时打印 panic 信息和调用栈。但在实际项目里,很多时候不会希望服务直接崩掉,而是希望在 recover 捕获 panic 后,把异常信息和调用栈记录到日志里,这时可以使用标准库 runtime/debug 包里的 debug.Stack()

package main

import (
    "fmt"
    "runtime/debug"
)

func safeRun() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("recover panic:", err)
            fmt.Println(string(debug.Stack()))
        }
    }()

    handler()
}

func handler() {
    service()
}

func service() {
    queryUser()
}

func queryUser() {
    panic("user id is empty")
}

func main() {
    safeRun()
}

运行后可以看到类似下面的堆栈信息:

Go 异常调用栈展示

堆栈信息一般从上往下展示“当前正在执行的位置”,例如 runtime/debug.Stack()safeRun.func1()panic(...)。如果想看业务调用链,通常从下往上读:main 调用 safeRunsafeRun 调用 handler,再进入 service,最后在 queryUser 里触发 panic。

需要注意的是,debug.Stack() 主要用于 panic、recover、日志记录这类场景。普通 error 不会自动携带调用栈,如果想让普通错误也带堆栈,需要在创建错误时主动记录,或者使用支持堆栈的第三方错误库。

支持堆栈的第三方错误库

如果希望普通 error 也能像异常一样打印调用栈,可以使用第三方错误库。它们的核心思路都是:在创建错误或包装错误时,把当前调用点的堆栈保存到 error 对象里;后续打印错误时,再把堆栈信息一起输出。

github.com/pkg/errors

github.com/pkg/errors 是 Go 里比较经典的错误处理库,常见于老项目。它的 Wrap 会同时做两件事:给原始错误添加上下文信息,并记录当前调用点的堆栈。

安装:

go get github.com/pkg/errors

示例:

package main

import (
    "fmt"
    "os"

    pkgerrors "github.com/pkg/errors"
)

func readConfig() error {
    _, err := os.Open("./config.yaml")
    if err != nil {
        return pkgerrors.Wrap(err, "read config failed")
    }

    return nil
}

func startApp() error {
    if err := readConfig(); err != nil {
        return pkgerrors.Wrap(err, "start app failed")
    }

    return nil
}

func main() {
    if err := startApp(); err != nil {
        fmt.Printf("%+v\n", err)
    }
}

这里要注意打印格式:

fmt.Println(err)        // 只打印错误链文本
fmt.Printf("%+v\n", err) // 打印错误链 + 堆栈信息

输出会类似:

open ./config.yaml: no such file or directory
read config failed
main.readConfig
        D:/demo/main.go:12
main.startApp
        D:/demo/main.go:20
main.main
        D:/demo/main.go:28
runtime.main
        C:/Program Files/Go/src/runtime/proc.go:267
start app failed
main.startApp
        D:/demo/main.go:21
main.main
        D:/demo/main.go:28
runtime.main
        C:/Program Files/Go/src/runtime/proc.go:267

如果只想给已有错误补堆栈,不想额外添加文案,可以使用 WithStack

if err != nil {
    return pkgerrors.WithStack(err)
}

不过这个库已经进入维护模式,新项目如果只是补充上下文,优先使用标准库 fmt.Errorf("%w", err);如果明确需要 error 自带堆栈,再考虑这类第三方库。

github.com/cockroachdb/errors

github.com/cockroachdb/errors 是功能更完整的错误库,也支持 WrapWrapfWithStack,并且兼容标准库的 errors.Is / errors.As 判断方式。

安装:

go get github.com/cockroachdb/errors

示例:

package main

import (
    stderrors "errors"
    "fmt"
    "os"

    crerrors "github.com/cockroachdb/errors"
)

func readConfig() error {
    _, err := os.Open("./config.yaml")
    if err != nil {
        return crerrors.Wrap(err, "read config failed")
    }

    return nil
}

func main() {
    err := readConfig()
    if err != nil {
        fmt.Printf("%+v\n", err)

        if stderrors.Is(err, os.ErrNotExist) {
            fmt.Println("配置文件不存在")
        }
    }
}

Wrap 适合在错误向上返回时添加业务语义;WithStack 适合只补堆栈,不改变错误文案的场景:

if err != nil {
    return crerrors.WithStack(err)
}

实际项目里不建议每一层都无脑 Wrap,否则错误信息会非常长。一般只在模块边界、业务语义发生变化的位置补充上下文,例如“读取配置失败”“初始化数据库失败”“调用支付接口失败”。这样既能保留堆栈,又不会让日志被重复包装淹没。

error 处理最佳实践

  1. 函数最后一个返回值放 error
  2. 有 error 必须优先处理,不要忽略
  3. 错误信息简洁、明确、带上下文
  4. 不要嵌套过多错误,向上层返回原始错误即可

panic

panic 用于致命错误,触发后:立即停止当前函数执行,逐层向上执行 defer(延迟函数),最终程序崩溃打印堆栈信息

func panic(v any)

来看个例子:

func start() {
	fmt.Println("程序启动")
	panic("发生致命错误!") // 触发异常
	fmt.Println("这行代码不会执行")
}

func main() {
	start()
	fmt.Println("程序结束") // 不会执行
}

panic在以下这些场景不需要手动调用,Go 运行时会自动触发:

  1. 数组 / 切片越界
  2. 空指针调用
  3. 关闭已关闭的通道
  4. 类型断言失败

捕获异常:recover

recover 是 Go 唯一能恢复 panic 的机制,让程序不崩溃,必须配合 defer 使用。

package main

import "fmt"

func test() {
	// 必须在 defer 中调用 recover
	defer func() {
		// 捕获 panic
		if err := recover(); err != nil {
			fmt.Println("捕获到异常:", err)
			// 在这里可以做日志记录、资源释放等操作
		}
	}()

	fmt.Println("执行任务")
	panic("空指针异常") // 触发异常
	fmt.Println("异常后代码") // 不会执行
}

func main() {
	test()
	// 程序恢复执行,不会崩溃
	fmt.Println("程序继续运行")
}

recover() 必须写在 defer 函数内,否则无效
捕获后,程序会从 panic 位置恢复,继续执行后续代码
只能捕获当前 goroutine 内的 panic

错误容易被忽略,编译器不检查

error 是作为返回值来抛出的,如果只是调用方法,不管返回值,那么这个error就相当于被忽略了,编译器也不会检查,例如:

os.Remove("file") // 错误被默默丢弃

多线程与并发

Goroutines

Goroutine 是 Go 语言实现的用户态轻量级线程。创建 Goroutine 就可以简单理解为创建线程。每个 goroutine 在执行时都是独立的,但它们共享相同的内存空间。使用 go 关键字创建 Goroutine

// 普通函数
go task()

// 匿名函数最常用
go func() {
    // 异步逻辑
}()

举个例子:

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    for i := 0; i < 5; i++ {
        fmt.Println("Hello from goroutine!")
        time.Sleep(1 * time.Second)
    }
}

func main() {
    go sayHello() // 启动 goroutine
	fmt.Println("Hello from main!")
	// 必须等待!否则 main 协程结束,子协程会被直接杀死
	time.Sleep(1 * time.Second)
}

goruntine 无需池化也高性能,而 java 中的线程一般不新建,需要使用线程池池化复用

协程里 panic不捕获会整个程序崩溃,必须手动 recover(),

go func(){
    defer func(){
        if e:=recover();e!=nil{
            // 捕获异常,主程序不崩
        }
    }()
    panic("err")
}()

和Java对比:Java 线程异常隔离互不影响,子线程异常一般不会导致主线程崩溃,可自定义 UncaughtExceptionHandler 捕获线程异常

Channels

Channels 是 Go 中用于在 goroutines 之间进行通信的机制,保证数据的安全性。

package main

import (
    "fmt"
)

func calculateSquare(n int, ch chan int) {
    ch <- n * n // 发送结果到 Channel
}

func main() {
    ch := make(chan int) // 创建int类型的 Channel

    for i := 1; i <= 5; i++ {
        go calculateSquare(i, ch) // 启动多个 goroutine
    }

    for i := 1; i <= 5; i++ {
        result := <-ch // 从 Channel 接收结果,阻塞等待数据,直到协程发送结果
        fmt.Println("Square:", result)
    }
}

WaitGroup

WaitGroup用于等待一组 goroutines 完成,方便管理并发任务。这个对比Java中的Thread.join, Object#wati等 API

package main

import (
	"fmt"
	"sync"
)

func printNumber(n int, wg *sync.WaitGroup) {
	defer wg.Done() // 完成后通知 WaitGroup
	fmt.Println("Done ", n)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1)              // 增加 WaitGroup 计数
		go printNumber(i, &wg) // 启动 goroutine
	}

	wg.Wait() // 等待所有 goroutine 完成
}

defer

defer 用于延迟函数调用,有以下特性:

  1. 被 defer 修饰的函数调用,不会立刻执行,而是等到当前函数执行完毕(return 之前)才执行。defer 在函数 return 语句执行之后、真正返回调用方之前 运行
  2. 多个 defer 按 后进先出 LIFO 栈顺序 执行
package main

import "fmt"

func main() {
	defer fmt.Println("defer 11111111")
	fmt.Println("111111111") // 最先执行
	defer fmt.Println("defer 22222222")
	defer fmt.Println("defer 33333333")
}

结果:
111111111
defer 33333333
defer 22222222
defer 11111111

规则 1:defer 注册时参数立刻求值,不是延迟时求值

常见使用场景

  1. 资源释放:文件、连接、锁、关闭流,类似java的finally
func readFile() {
    f, err := os.Open("test.txt")
    if err != nil {
        return
    }
    // 提前注册关闭,函数退出自动关文件
    defer f.Close()

    // 中间业务逻辑...
}
  1. 解锁
var mu sync.Mutex

func do() {
    mu.Lock()
    defer mu.Unlock() // 不管中间怎么return,一定解锁
    // 临界区操作
}
  1. 捕获协程 panic(配合 recover)
func safeGoroutine() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获panic:", err)
        }
    }()

    panic("出错了")
}

下面看一个 defer 关键字的一个问题:x := f() 的结果为啥是 6 ?

package main

import "fmt"

func f() (x int) {
	defer func() {
		x++
	}()
	return 5
}

func main() {
	x := f()  
	fmt.Println(x) // x = 6
}

执行流程:
return 5 → 给有名返回值 x 赋值为 5
执行 defer 匿名函数:x++ → x=6
函数真正返回 x

posted @ 2025-07-25 23:53  vonlinee  阅读(51)  评论(0)    收藏  举报