go 基础中的一些坑

类型转换

go 语言中,类型转换是显式的,不会自动转换

go 复制代码
func main(){
  i := 100
  var f float64
  f = float64(i)
}

string 转换成 int 需要借助 strconv

使用 strconv.Atoi 函数将 string 转换成 int,转换后它会输出两个值:

  • 一个是转换后的值
  • 一个 error,如果转换出错了,第一个值是 0
go 复制代码
func main() {
  str := "100s"
  i, err := strconv.Atoi(str)
  if err != nil {
    fmt.Println(err)
  }
  fmt.Println(i)
}

这个包还提供了其他的类型的字符串转换方式

  • strconv.ParseBool,将字符串 bool 转换成 bool 类型
  • strconv.ParseFloat,将字符串 float 转换成 float 类型
  • strconv.ParseInt,将字符串 int 转换成 int 类型
  • strconv.ParseBool,将 bool 类型转成 string 类型

数组

go 中数组是定长的,也就是说你需要指定数组的长度,未初始化的项默认值是 0

数组需要注意一个越界问题,也就是说不能访问数组的长度之外的元素

go 复制代码
func main() {
  // 10 个元素的数组
  arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  fmt.Println(arr[9]) // 10
  fmt.Println(arr[10]) // 会报错
  arr2 := [10]int{1, 2, 3}, // 从下标 4 开始到下标 9 的元素都是 0
}

数组的长度是不可改变的

go 复制代码
func main() {
  arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  arr = [10]int{1, 2, 3} // 数组的长度是不能改变的
}

slice

slice 是一种动态数组,它是由相同类型元素组成的序列

slice 底层由三部分组成:

  1. 指向底层数组的指针
  2. slice 的长度
  3. slice 的容量

在底层数组没有被扩展之前,slice 的长度和容量相等,当我们向 slice 中添加元素时,如果超过了 slice 的容量,那么 go 就会重新分配一个更大的底层数组,并将原始数组复制到心的数组中,这个过程被称为扩容

初始化一个 slice 有两种方式:

  1. 直接使用字面量的方式初始化一个 slice
go 复制代码
func main() {
  s := []int{1, 2, 3, 4}
}
  1. 使用 make 函数创建一个 slice,它接收三个参数:
  • 第一个参数是类型
  • 第二个参数是长度
  • 第三个参数是容量
    • 第三个参数可以省略,也就是说如果不指定容量,那么容量等于长度
go 复制代码
func main() {
  s := make([]int, 3, 5) // 创建一个长度为 3,容量为 5 的切片
}

slice 的访问方式和数组一样:

  1. 通过下标访问
go 复制代码
arr := [4]int{1, 2, 3, 4}
fmt.Println(arr[0]) // 1
s := []int{1, 2, 3, 4}
fmt.Println(s[0]) // 1
  1. 通过区间访问(前闭后开)
go 复制代码
arr := [4]int{1, 2, 3, 4}
fmt.Println(arr[0:2]) // 1,2
s := []int{1, 2, 3, 4}
fmt.Println(s[0:2]) // 1,2
  • 区间访问的时候,如果省略了开始的下标,那么默认是 0
  • 如果省略了结束的下标,那么默认是 slice 的长度
  1. 不能越界访问

删除 slice 中的某项元素,可以使用 append 函数,三个点是展开操作符,它会将 s 中的元素展开

go 复制代码
s := []int{1, 2, 3, 4, 5, 6, 7, 8}
s = append(s[:1], s[2:]...)

数组和 slice 最大的区别是,数组的长度是固定的,而 slice 的长度是可变的

go 复制代码
arr := [8]int{1, 2, 3, 4, 5, 6, 7, 8} // 数组
s := []int{1, 2, 3, 4, 5, 6, 7, 8} // 切片

数组可以转变成切片

go 复制代码
arr := [8]int{1, 2, 3, 4, 5, 6, 7, 8} // 数组
s := arr[:] // 将数组转换成切片

slice 是引用类型

如果一个函数接收的是切片,那么它接收的是切片的引用,也就是说它会改变原始切片的值

go 复制代码
func main() {
  s := []int{1, 2, 3, 4, 5, 6, 7, 8}
  fmt.Println(s)  // [1 2 3 4 5 6 7 8]
  addOne(s)
  fmt.Println(s)  // [2 3 4 5 6 7 8 9]
}

// 接收的事切片的引用
func addOne(n []int) {
  for i := 0; i < len(n); i++ {
    n[i] = n[i] + 1
  }
}

如果函数接收的是数组,那么它接收的是数组的值,也就是说它不会改变原始数组的值

go 复制代码
func main() {
  s := [8]int{1, 2, 3, 4, 5, 6, 7, 8}
  fmt.Println(s)  // [1 2 3 4 5 6 7 8]
  addOne(s)
  fmt.Println(s)  // [1 2 3 4 5 6 7 8]
}

func addOne(n [8]int) {
  for i := 0; i < len(n); i++ {
    n[i] = n[i] + 1
  }
}

那么数组要实现引用传递怎么办呢?可以使用指针

go 复制代码
func main() {
  s := [8]int{1, 2, 3, 4, 5, 6, 7, 8}
  fmt.Println(s)  // [1 2 3 4 5 6 7 8]
  addOne(&s)
  fmt.Println(s)  // [2 3 4 5 6 7 8 9]
}

func addOne(n *[8]int) {
  for i := 0; i < len(n); i++ {
    n[i] = n[i] + 1
  }
}

最后,slice 如何传递指针?在函数内部需要使用 * 来解引用,然后在对 slice 进行操作

go 复制代码
func main() {
  s := []int{1, 2, 3, 4, 5, 6, 7, 8}
  fmt.Println(s)
  addOne(&s)
  fmt.Println(s)
}

func addOne(n *[]int) {
  // 先对 n 解引用
  _n := *n
  for i := 0; i < len(_n); i++ {
    _n[i] = _n[i] + 1
  }
}

map

map 未初始化,可以取值,但赋值会报错,

go 复制代码
func main() {
  var m1 map[string]int
  fmt.Println(m1["age"]) // 0
  m1["age"] = 1 // 报错
}

map 的初始化有两种方式:

go 复制代码
m1 := map[string]int{"age": 1}  // 字面量的方式
m2 := map[string]int{}          // 这种也是字面量
m3 := make(map[string]int)      // 使用 make 函数

如何判断一个属性在不在 map 中呢?可以使用 ok 来判断

go 复制代码
func main() {
  m2 := make(map[string]int)
  m2["age"] = 25
  a, ok := m2["age"]
  fmt.Println(a, ok)  // 25 true
  a, ok = m2["age2"]
  fmt.Println(a, ok)  // 0 false
}

switch

go 中的 switch 如果命中某条 case 语句后,就不会命中其他 case 语句了

switch 可以使用 x.(type) 的方式类判断一个变量的类型,x.(type) 只能在 switch 语句中使用,不能在 if 语句中使用

go 复制代码
func typeof(x interface{}) {
  switch x.(type) {
  case int:
    fmt.Println("int")
  case string:
    fmt.Println("string")
  default:
    fmt.Println("unknown")
  }
}

for

for 循环中,如果操作指针的话,会有一个问题,如下所示:添加到 oddNumbers 中都是 7

因为 number 是一个变量,它的地址是不变的,所以 oddNumbers 中的元素都是 number 的地址,而 number 最后的值是 7

go 复制代码
func main() {
  numbers := []int{1, 2, 3, 4, 5, 6, 7}

  var oddNumbers []*int
  for _, number := range numbers {
    // number 的地址是不变的
    oddNumbers = append(oddNumbers, &number)
  }

  for _, oddNumber := range oddNumbers {
    fmt.Println(*oddNumber)
  }
}

如何解决这个问题呢?可以使用一个临时变量,每一个循环进来的时候,都创建一个临时变量,然后将它的地址添加到 oddNumbers

go 复制代码
func main() {
  numbers := []int{1, 2, 3, 4, 5, 6, 7}

  var oddNumbers []*int
  for _, number := range numbers {
    // 每次循环都会创建一个新变量,然后将它的地址添加到 oddNumbers 中
    tmp := number
    oddNumbers = append(oddNumbers, &tmp)
  }

  for _, oddNumber := range oddNumbers {
    fmt.Println(*oddNumber)
  }
}

error

任何一个实现了 Error() 方法的类型都可以作为错误类型

go 复制代码
type MyError struct {
  message string
  code    int
}

func (e MyError) Error() string {
  return e.message
}

func add() (*int, error) {
  var myError = MyError{
    message: "This is an error",
    code:    500,
  }
  return nil, myError  // 使用自定义的错误
}

判断一个 error 是什么类型最好使用 errors.Is 函数,不要用 == 来判断

go 复制代码
e1 := fmt.Errorf("error 1: %w", io.EOF)
fmt.Println(errors.Is(e1, io.EOF))  // true
fmt.Println(e1 == io.EOF) // false

error 类型断言可以使用 errors.As 函数,不要使用 err.(xx)

因为 errors.Aserrors.Is 函数是可以判断包装过的 error

package

在一个 package 中执行顺序:

  1. 先执行 const 常量
  2. 再执行 var 变量
  3. 然后再执行 init 函数
  4. 最后执行 main 函数

如果有引入其他的 package,那么它会先执行其他的 packageconstvarinit,然后再执行当前 packageconstvarinit,这是一个深度优先的顺序

go mod 是一个用于管理 go 的依赖模块,它会将依赖的模块下载到 go 的缓存目录中,然后在 go.mod 文件中记录下来,一般存储在 $GOPATH/pkg/mod 目录中

go mod 的命令:

  • go mod init:初始化一个 go.mod 文件
  • go mod tidy:根据 go.mod 文件整理依赖
  • go mod download:下载 go.mod 文件中的依赖,但不安装
  • go mod verify:验证依赖是否正确和完整
  • go mod graph:以图形化显示模块之间的依赖关系
  • go mod why:解释模块为什么需要特定的依赖

receiver

指针类型 receiver 使用指向该类型的指针作为接收者;值类型 receiver 使用该类型的值作为接收者

定义一个 Animal 接口,它有一个 Eat 方法,定义 Dog 结构体,它有一个 Say 方法,然后实现 Animal 接口

使用指针类型作为 receiver,实现接口需要使用指针类型的值

go 复制代码
type Animal interface {
  Eat()
}
type Dog struct {
  Name string
}
func (d *Dog) Say() {
  fmt.Printf("Name is %v", d.Name)
}
func main() {
  // 这里只能使用指针类型的值
  var a Animal = &Dog{Name: "uccs"}
  a.Say()
}

使用值类型作为 receiver,实现接口可以使用值类型的值,也可以使用指针类型的值

go 复制代码
type Animal interface {
  Say()
}
type Dog struct {
  Name string
}
func (d Dog) Say() {
  fmt.Printf("Name is %v", d.Name)
}
func main() {
  // 这里可以使用指针类型的值
  var a Animal = &Dog{Name: "uccs"}
  a.Say()

  // 也可以使用值类型的值
  var a1 Animal = Dog{Name: "astak"}
  a1.Say()
}

interface

接口类型断言:

go 复制代码
func AnimalSleep(e Eater){
  if s, ok := e.(Animal); ok {
    s.Sleep()
  }
}

对于 interface 类型的变量,不能使用指针类型的零值(指针类型的零值是 nil):你会处理 go 中的 nil 吗

抽象接口的实现:

go 复制代码
type Worker interface {
  doWork()
  Start()
}
type BaseWorker struct {
  Worker
}
func (b *BaseWorker) Start() {
  fmt.Println("start")
  b.doWork()
  fmt.Println("end")
}
type NormalWorker struct {
  BaseWorker
}
func (n *NormalWorker) doWork() {
  fmt.Println("do work")
}
func NewNormalWorker() Worker {
  n := &NormalWorker{BaseWorker{}}
  // 这边需要赋值,不然会报错
  n.Worker = n
  return n
}
func main() {
  NewNormalWorker().Start()
}

goroutine 中的竞态

当有多个 goroutine 对同一个变量进行读写操作时,就会出现竞态

因为写入一个变量的操作不是原子的,一般会分为三步:

  1. 读取变量的值:read counter
  2. 对变量的值进行操作:counter = counter + 1
  3. 将最新的值写入变量:write counter
go 复制代码
var counter int32
func main() {
  var wg sync.WaitGroup
  for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func(i int) {
      counter = counter + 1
      wg.Done()
    }(i)
  }
  wg.Wait()
  fmt.Println(counter) // 不一定是 1000
}

如何解决这个问题呢:

  1. 使用 atomic 包中的原子操作

    • x86 架构中,atomic.AddInt32 函数使用 lock xaddq 指令来实现原子加操作
    go 复制代码
    var counter int32
    func main() {
     var wg sync.WaitGroup
     for i := 0; i < 1000; i++ {
       wg.Add(1)
       go func(i int) {
         // 这个操作是原子的
         atomic.AddInt32(&counter, 1)
         wg.Done()
       }(i)
     }
     wg.Wait()
     fmt.Println(counter)
    }

    这两种方法是一样的,只是 atomic.AddInt32 的方法更加简洁

    go 复制代码
    var counter = atomic.Int32{}
    func main() {
      var wg sync.WaitGroup
      for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(i int) {
          counter.Add(1)
          wg.Done()
        }(i)
      }
      wg.Wait()
      fmt.Println(counter.Load())
    }
  2. atomic.CompareAndSwapInt64 函数是使用 CPU 的原子指令来实现的,它使用了 CPU 提供的 compare-and-swap 指令来保证原子性

    • CAS 指令可以原子的比较并交换一个内存地址中的值,它有三个操作数:内存地址 addr,期望的旧值 old,新值 new
    • 如果内存地址 addr 中的值等于 old,那么将 new 的值写入 addr 中,否则不做任何操作
    go 复制代码
    var counter int32
    func main() {
      var wg sync.WaitGroup
      for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(i int) {
          for {
            if swaped := atomic.CompareAndSwapInt32(&counter, counter, counter+1); swaped {
              break
            }
          }
          wg.Done()
        }(i)
      }
      wg.Wait()
      fmt.Println(counter)
    }
  3. 使用锁

    go 复制代码
    var counter int32
    var lock sync.Mutex
    func main() {
      var wg sync.WaitGroup
      for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(i int) {
          lock.Lock()
          counter = counter + 1
          lock.Unlock()
          wg.Done()
        }(i)
      }
      wg.Wait()
      fmt.Println(counter)
    }
相关推荐
猪猪拆迁队16 分钟前
虚拟工厂仿真引擎的架构设计:让一条产线可编程、可观测、可干预
后端·ai编程
字节跳动数据库40 分钟前
文章分享——相似函数处理方法
人工智能·后端·程序员
云技纵横40 分钟前
@Transactional 失效的 7 种场景:第 5 种最难排查
后端
用户6757049885021 小时前
你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!
后端·go
程序员cxuan1 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
用户6757049885021 小时前
面试官问“装饰器模式”,这样回答薪资多要 3000!
后端
tntxia1 小时前
Geo Scene域名修改引起的一些问题
后端
用户298698530141 小时前
Java 实现 Word 文档加密与权限解除
java·后端
vanuan2 小时前
给你的A2A-Agent加把锁-认证鉴权实战指南
后端
Yeats_Liao2 小时前
14:Servlet中的页面跳转-Java Web
java·后端·架构