My Little World

语法 1

变量、常量、数据类型

1. 语法讲解

概念 是什么 解决什么问题 语法示例
变量 运行时可改的数据容器 存储中间结果、传递数据 var n int = 10
常量 编译时确定且不可改 统一配置、防止魔数 const pi = 3.14
基本类型 语言内置的单一值类型 决定内存大小与操作 int float64 bool string
复合类型 由基本类型组合而成 描述复杂实体 []int map[string]int struct{}
类型转换 显式把 A 类型变成 B 类型 避免类型不匹配编译错 float64(age)

2. 其他用法与总结

其他用法

用法 是什么 为什么 30 秒代码
匿名变量 _ 占位符,丢弃值 避免“未使用”编译错;多返回值时干净 _, err := fmt.Println("ok")
平行赋值 同时给多变量赋值 无需临时变量即可交换;一键解包 a, b = b, a
常量表达式 常量间运算 魔数收敛,一改全改;编译期计算零运行时开销 const MB = 1024 * 1024
新类型 vs 别名 type Meter float64 创建新类型;type Second = float64 仅别名 强类型防止单位混淆;编译器帮你找 bug func distance(d Meter) 传错类型就报错
range遍历 rune = Unicode 码点 按“字符”而非字节遍历,汉字不劈断 for _, r := range "你好" { fmt.Printf("%c\n", r) }

总结

  1. 变量:三种写法,记得零值。
    i. 标准写法 var s1 string = "hello"
    ii. 类型推导 var s1 = "hello"
    iii. 简写 s1 := "hello"
  2. 常量:const + iota 做枚举,编译期确定。
  3. 类型:
    • 基本类型决定内存大小和可进行的操作;
    • 复合类型描述复杂结构;
    • 自定一类型防止混淆
    • 转换必须显式。
  4. 技巧:
    • _ 丢弃、平行赋值、常量表达式、新类型防混淆、rune 遍历 Unicode。
1
2
3
4
5
6
7
8
9
10
11
12
type Person struct {
Name string
Age int
}

func (p Person) String() string {
return fmt.Sprintf("Name: %s, Age: %d", p.Name, p.Age)
}

func (p *Person) String2() string {
return fmt.Sprintf("Name: %s, Age: %d", p.Name, p.Age)
}

String() 使用的是值接收者,String2() 使用的是指针接收者。加 * 是为了传递内存地址而不是复制整个数据。

一、 语法层面的区别(最直观的差异)

  • func (p Person) String():p 是调用该方法时原对象的一个副本(Copy)。在 String() 内部修改 p.Name,不会影响原来的 Person 对象。
  • func (p *Person) String2():p 是原对象在内存中的地址(指针)。在 String2() 内部修改 p.Name,会直接改变原来的 Person 对象。

二、 调用机制的区别(Go 语言的语法糖)

在实际写代码时,你可能会发现无论用值还是指针,好像都能调用成功:

1
2
3
p := Person{Name: "张三", Age: 18}
fmt.Println(p.String()) // 正常调用值接收者
fmt.Println(p.String2()) // 看起来是用值调用了指针接收者?

为什么 p.String2() 不报错?
因为 Go 语言有自动取地址的机制。当使用值 p 调用指针接收者方法 String2() 时,Go 会自动把它转换为 (&p).String2()。
反之,如果是用指针 ptr := &p 调用值接收者 String(),Go 会自动解引用:(*ptr).String()。

但是,接口实现(Interface)不吃这一套!
这是最关键的考点:

  • Person 类型的值,既拥有 String() 方法,也拥有 String2() 方法(因为编译器帮你自动转换了)。
  • 但是,如果有一个接口 type I interface { String() string },只有 Person 类型(值) 实现了这个接口。
  • 如果是接口 type I2 interface { String2() string },只有 *Person 类型(指针) 实现了这个接口。把 Person 的值赋给 I2 会直接报编译错误。

三、 为什么要这么设计?(应用场景与最佳实践)

为什么有时候加 *,有时候不加?在工程实践中,有一条铁律:

👉 如果方法内部需要修改接收者的状态,必须用指针接收者(*T)。
👉 如果接收者是一个大的结构体(包含很多字段),为了性能避免数据拷贝,建议用指针接收者(*T)。
👉 如果接收者是基本类型、小结构体,且不需要修改原对象,用值接收者(T)更安全。

结合你图片里的代码:

  1. 关于 String()(值接收者):
    这个方法只是用来格式化输出字符串(类似 Java 的 toString()),它不需要修改 Person 的 Name 或 Age。所以用值接收者是合理的,保证了该方法的“只读”特性。

  2. 关于 String2()(指针接收者):
    这个方法里面的逻辑和 String() 一模一样,并没有修改任何字段。所以单从这个方法的功能看,用指针接收者是多余的(除非 Person 结构体极其庞大,为了避免每次调用拷贝内存)。

结论

只需要读值的时候就用值传递
如果需要修改值进行写入时就用指针传递

代码练习

函数

1. 语法讲解

Go语言函数的基本语法:

1
2
3
4
func 函数名(参数列表) 返回值列表 {
// 函数体
return 返回值
}

注意区别给结构体加方法的语法:

1
2
3
4
5
type 结构体名 struct {}
func (接收者 *结构体名) 方法名(参数列表) 返回值列表 {
// 方法体
return 返回值
}

特点:

支持多返回值
支持命名返回值
支持可变参数
函数可以作为值传递
匿名函数与闭包:

1
2
3
4
5
6
// 匿名函数
func(参数列表) 返回值列表 {
// 函数体
}

// 闭包:函数内部定义的函数,可以访问外部函数的变量

2. 应用场景

普通函数:用户注册验证、数据格式转换
多返回值函数:数据库操作(返回结果和错误)
匿名函数:并发任务、排序比较函数
闭包:计数器、缓存实现、中间件

练习

指针

1. 语法讲解

Go语言指针的基本概念:

  • 内存地址:每个变量在内存中都有唯一的地址
  • 指针变量:存储另一个变量内存地址的变量
  • 指针操作:
1
2
3
var p *int        // 声明指针
p = &variable // 取地址
*p = 100 // 解引用(通过指针改变量值)

指针作为函数参数:

  • 值传递:函数内修改不影响原变量
  • 指针传递:函数内修改影响原变量

2. 应用场景

  • 指针传递:大对象传递(避免值拷贝开销)
  • 修改原变量:函数内需要修改调用者的变量
  • 动态内存分配:构建复杂数据结构(链表、树等)
  • 节省内存:避免大数据结构的重复拷贝

练习

错误处理

Go语言的错误处理机制:

  • error类型:内置接口类型
1
2
3
type error interface {
Error() string
}
  • defer语句:延迟执行,后进先出
1
2
3
defer func() {
// 延迟执行的代码
}()
  • panic/recover机制:
1
2
3
4
5
// 触发panic
panic("错误信息")

// 恢复panic
func recover() interface{}

2. 应用场景

  • error类型:数据库连接失败、文件读写错误
  • defer语句:资源清理(关闭文件、释放锁、数据库连接)
  • panic/recover:处理不可恢复的错误(如数组越界) 仅针对当前协程

练习