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)
    }
相关推荐
2401_8581205311 分钟前
古典舞在线交流平台:SpringBoot设计与实现详解
java·spring boot·后端
赐你岁月如歌24 分钟前
如何使用ssm实现基于web的网站的设计与实现+vue
java·后端·ssm
潘多编程2 小时前
Spring Boot微服务架构设计与实战
spring boot·后端·微服务
2402_857589362 小时前
新闻推荐系统:Spring Boot框架详解
java·spring boot·后端
2401_857622662 小时前
新闻推荐系统:Spring Boot的可扩展性
java·spring boot·后端
江湖十年4 小时前
在 Go 中如何优雅的处理错误
后端·go
Amagi.4 小时前
Spring中Bean的作用域
java·后端·spring
侠客行03174 小时前
xxl-job调度平台之任务触发
java·后端·源码
2402_857589364 小时前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端
J老熊4 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构