目录
[指针:传值 vs 传指针](#指针:传值 vs 传指针)
[错误处理:error、%w、Is / As](#错误处理:error、%w、Is / As)
函数:多返回值、命名返回、可变参数
Go
package main
import "fmt"
// 普通函数
func add(a, b int) int {
return a + b
}
// 多返回值:Go 里极常见,尤其是 (结果, error)
func div(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为 0")
}
return a / b, nil
}
// 命名返回值:return 时可裸 return(初学建议少用,可读性一般)
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // 等价于 return x, y
}
// 可变参数:内部当切片用
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(add(1, 2))
result, err := div(10, 2)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("商 =", result)
a, b := split(17)
fmt.Println(a, b)
fmt.Println(sum(1, 2, 3, 4))
// 把切片展开传给可变参数
arr := []int{1, 2, 3}
fmt.Println(sum(arr...))
}
工作习惯
Go
result, err := someCall()
if err != nil {
return err // 或包装后返回
}
// 使用 result
指针:传值 vs 传指针
Go
package main
import "fmt"
// 传值:函数内改的是副本,外层不变
func addOneByValue(n int) {
n = n + 1
}
// 传指针:可以修改外层变量
func addOneByPointer(n *int) {
*n = *n + 1 // * 解引用:取指针指向的值并赋值
}
type User struct {
Name string
Age int
}
// 大结构体常用指针,避免拷贝;也可在函数内修改字段
func birthday(u *User) {
if u == nil {
return // 使用指针前先判空
}
u.Age++
}
func main() {
x := 10
addOneByValue(x)
fmt.Println(x) // 仍是 10
addOneByPointer(&x) // & 取地址
fmt.Println(x) // 变成 11
p := &x
fmt.Println(p) // 地址
fmt.Println(*p) // 通过指针读值:11
u := User{Name: "Tom", Age: 20}
birthday(&u)
fmt.Println(u.Age) // 21
}
何时用指针
- 需要在函数内修改原变量
- 结构体较大,避免拷贝
- 表示"可能没有":用
nil(但要注意空指针)
结构体、方法(值/指针接收者)
Go
package main
import "fmt"
type Person struct {
Name string
Age int
}
// 值接收者:方法内改动不影响原对象(操作的是副本)
func (p Person) Greeting() string {
return "Hi, I am " + p.Name
}
// 指针接收者:可修改原对象;也避免大对象拷贝
func (p *Person) GrowOlder() {
p.Age++
}
// 即使是值接收者,Go 允许用指针调用;反之亦然(有规则)
// 经验法则:要改字段 → 用指针接收者;方法较多时保持风格统一
func NewPerson(name string, age int) *Person {
// 构造函数惯例:返回指针
return &Person{Name: name, Age: age}
}
func main() {
// 多种初始化方式
var p1 Person // 零值:Name="", Age=0
p2 := Person{Name: "Alice", Age: 18}
p3 := Person{Name: "Bob"} // Age 为 0
p4 := &Person{Name: "Carl", Age: 30}
fmt.Println(p1, p2, p3, p4.Greeting())
p2.GrowOlder() // 编译器会自动取地址:(&p2).GrowOlder()
fmt.Println(p2.Age)
p := NewPerson("Daisy", 22)
fmt.Println(p.Greeting(), p.Age)
}
嵌入(组合,类似"继承"的替代)
Go
type Animal struct {
Name string
}
func (a Animal) Speak() {
fmt.Println(a.Name, "makes a sound")
}
type Dog struct {
Animal // 匿名嵌入:提升字段和方法
Breed string
}
func demoEmbed() {
d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Husky",
}
d.Speak() // 可直接调用嵌入类型的方法
fmt.Println(d.Name) // 可直接访问嵌入字段
}
切片:创建、追加、截取、共享底层数组
切片是工作中最常用的序列类型(数组较少直接用)。
Go
package main
import "fmt"
func main() {
// ---------- 创建 ----------
var s1 []int // nil 切片,len=0, cap=0
s2 := []int{1, 2, 3} // 字面量
s3 := make([]int, 3) // len=3, cap=3,元素为 0
s4 := make([]int, 3, 5) // len=3, cap=5
fmt.Println(s1 == nil) // true
fmt.Println(len(s2), cap(s2))
fmt.Println(s3, s4)
// ---------- 追加 ----------
s2 = append(s2, 4)
s2 = append(s2, 5, 6)
fmt.Println(s2)
// ---------- 截取:s[low:high],不含 high ----------
a := []int{0, 1, 2, 3, 4, 5}
b := a[1:4] // [1 2 3]
fmt.Println(b)
// ★ 关键:截取后与原切片可能共享底层数组
b[0] = 99
fmt.Println(a) // a[1] 也变成 99
// 若要独立拷贝,用 copy
c := make([]int, len(b))
copy(c, b)
c[0] = 100
fmt.Println(b, c) // b 不受影响
// ---------- 删除下标 i 的元素(常用技巧)----------
i := 2
a = append(a[:i], a[i+1:]...)
fmt.Println(a)
// ---------- 遍历 ----------
for idx, v := range s2 {
fmt.Println(idx, v)
}
}
必记坑
append可能触发扩容,返回新切片,必须接住:s = append(s, x)- 截取共享底层数组,改子切片可能影响原切片
nil切片可以append,不必先make(读/写下标前要确保有长度)
map:增删查、遍历、初始化
Go
package main
import "fmt"
func main() {
// ---------- 初始化 ----------
var m1 map[string]int // nil map:可读不可写
m2 := map[string]int{} // 空 map,可写
m3 := make(map[string]int) // 推荐
m4 := map[string]int{
"alice": 90,
"bob": 80,
}
_ = m1
fmt.Println(m2, m3, m4)
// ---------- 增 / 改 ----------
m3["tom"] = 95
m3["tom"] = 96 // 改
// ---------- 查 ----------
score, ok := m3["tom"]
if ok {
fmt.Println("找到:", score)
} else {
fmt.Println("不存在")
}
// 直接取值:不存在时得到值类型零值(int 为 0),所以重要查询要用 ok
fmt.Println(m3["missing"]) // 0
// ---------- 删 ----------
delete(m3, "tom")
// ---------- 遍历(无序!)----------
for k, v := range m4 {
fmt.Println(k, v)
}
// ---------- 引用类型注意 ----------
m5 := m4
m5["alice"] = 100
fmt.Println(m4["alice"]) // 100:map 赋值是引用语义
}
必记坑
nilmap 不能写:先make或字面量初始化- map 不是并发安全的,多 goroutine 写要用锁或
sync.Map - 遍历顺序每次可能不同
接口:隐式实现、空接口、小接口
Go
package main
import "fmt"
// 接口:只定义行为(方法集合)
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
// Dog 实现了 Speak,无需写 implements
func (d Dog) Speak() string {
return d.Name + ": woof"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return c.Name + ": meow"
}
// 面向接口编程:参数用接口,而不是具体类型
func announce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
announce(Dog{Name: "Buddy"})
announce(Cat{Name: "Mimi"})
// ---------- 空接口:可装任何类型(Go 1.18+ 更推荐 any)----------
var anyValue any // 等价于 interface{}
anyValue = 1
anyValue = "hello"
anyValue = Dog{Name: "X"}
fmt.Println(anyValue)
// 类型断言
str, ok := anyValue.(string)
if ok {
fmt.Println(str)
}
// 类型 switch
switch v := anyValue.(type) {
case int:
fmt.Println("int", v)
case string:
fmt.Println("string", v)
case Dog:
fmt.Println("dog", v.Name)
default:
fmt.Println("other", v)
}
}
工作中的小接口思维
Go
// 好:接口小,只暴露需要的方法
type UserRepository interface {
FindByID(id int64) (*User, error)
Save(u *User) error
}
// handler/service 依赖接口,测试时可 mock
错误处理:error、%w、Is / As
Go
package main
import (
"errors"
"fmt"
)
// 哨兵错误:包级可导出变量,便于 errors.Is 判断
var ErrNotFound = errors.New("not found")
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func findUser(id int) (string, error) {
if id <= 0 {
return "", &ValidationError{Field: "id", Msg: "must be positive"}
}
if id == 404 {
return "", ErrNotFound
}
return "alice", nil
}
func serviceGetUser(id int) (string, error) {
name, err := findUser(id)
if err != nil {
// %w 包装错误,保留错误链,便于上层 Is/As
return "", fmt.Errorf("serviceGetUser: %w", err)
}
return name, nil
}
func main() {
_, err := serviceGetUser(404)
if err != nil {
// Is:判断错误链中是否包含某个哨兵错误
if errors.Is(err, ErrNotFound) {
fmt.Println("业务:用户不存在")
}
// As:判断错误链中是否有某类型,并取出
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("校验失败:", ve.Field, ve.Msg)
}
fmt.Println("完整错误:", err)
}
}
工作习惯
- 用返回值传错误,不要用 panic 当业务失败
- 包装时用
%w,日志里可打印完整链 - 上层用
Is/As做分支,不要靠字符串比较