Go 小白的十万个为什么

简介: Go 小白的十万个为什么

string


假设我们要修改类型为字符串变量的某个字符,如果是之前世界上最伟大的语言,那么可以直接这样(差点忘本不会写php了):

<?php
$name = "test";
$name[0] = "T";
var_dump($name);
// string(4) "Test"

在 go 中是不允许使用索引下标操作字符串变量中的字符的,会直接编译错误。

// 不被允许
  x := "test"
  x[0] = 'T'
  //cannot assign to x[0]


一般要修改我会转换成 byte。

package main
import "fmt"
func main() {
  s := "test"
  bytes := []byte(s)
  bytes[0] = 'T'
  fmt.Println("s的值:", string(bytes))
  // s的值: Test
}


array


我们来看这样一段程序,

import "fmt"
func main() {
  a := [3]int{10, 20, 30}
  changeArray(a)
  fmt.Println("a的值:", a)
}
func changeArray(items [3]int) {
  items[2] = 50
}
// a的值: [10 20 30]

答案并不会是 [10,20,30]。因为上面把数组传递给函数时,数组将会被复制,是一个值传递,那么之后的修改当然和之前的没有关系。


当然你可以传递数组指针,这样他们指向的就是同一个地址了。

func main() {
  a := [3]int{10, 20, 30}
  changeArray(&a)
  fmt.Println("a的值:", a)
}
//数组是值传递
func changeArray(items *[3]int) {
  items[2] = 50
}
//a的值: [10 20 50]

或者可以使用 slice。

package main
import "fmt"
func main() {
  s := []int{10, 20, 30}
  changeSlice(s)
  fmt.Println("s的值是:",s)
}
func changeSlice(items []int) {
  items[2] = 50
}
// s的值是: [10 20 50]

slice 本质上不存储任何数据,它只是描述基础数组的一部分。slice 在底层的结构是,

type slice struct {
  array unsafe.Pointer // 底层数组的指针位置
  len   int // 切片当前长度
  cap   int //容量,当容量不够时,会触发动态扩容的机制
}

当传递的是 slice,并且切片的底层结构 array 的值还是指向同一个数组指针地址时,对数组元素的修改会相互影响。


slice


看看下面的代码,

package main
import "fmt"
func main() {
  data := cutting()
  fmt.Printf("data's len:%v,cap:%v\n", len(data), cap(data))
}
func cutting() []int {
  val := make([]int, 1000)
  fmt.Printf("val's len:%v,cap:%v\n ", len(val), cap(val))
  return val[0:10]
}
// val's len:1000,cap:1000
// data's len:10,cap:1000

就像上面说的,当在原有 slice 的基础上截取出新的 slice。slice 将会引用原切片的底层数组。如果是一个大的切片,会导致内存的浪费。


我们可以通过额外定义一个容量大小合适的变量,然后通过 copy 操作。

package main
import "fmt"
func main() {
  data := cutting()
  fmt.Printf("data's len:%v,cap:%v\n", len(data), cap(data))
}
func cutting() []int {
  val := make([]int, 1000)
  fmt.Printf("val's len:%v,cap:%v\n ", len(val), cap(val))
  res := make([]int, 10)
  copy(res, val)
  return res
}
//val's len:1000,cap:1000
// data's len:10,cap:10


copy


既然上面出现了 copy,那么我们来看看 copy。

package main
import "fmt"
func main() {
  var test1, test2 []int
  test1 = []int{1, 2, 3}
  copy(test2, test1)
  fmt.Println("test2 的值:", test2)
}
// test2 的值: []

为什么会这样?


copy 复制的元素数目是两个切片中最小的长度。当前 test1 和 test2 的最小长度为 test2 的0,因此最终返回空切片,我们可以为 test2 分配长度。

package main
import "fmt"
func main() {
  var test1, test2 []int
  test1 = []int{1, 2, 3}
  test2=make([]int,len(test1))
  copy(test2,test1)
  fmt.Println("test2 的值:",test2)
}
// test2 的值: [1 2 3]


range


我们来看下面的代码,

package main
import "fmt"
func main() {
  res := []int{1, 2, 3}
  for _, item := range res {
    item *= 10
  }
  fmt.Println("res:", res)
}
// res: [1 2 3]

最终的值并没有想象中的 [10,20,30]。为什么?


因为在 go 中 range 第二个返回值实际上是一个值拷贝。

这也告诉我们当遍历切片类型为结构体时,需要避免这样的操作,此操作会消耗大量的内存,我们可以通过索引下标搞定。

package main
import "fmt"
func main() {
  res := []int{1, 2, 3}
  for index, _ := range res {
    res[index] *= 10
  }
  fmt.Println("res:", res)
  // res: [10 20 30]
}


struct


我们经常会使用 "==" 去判断两个结构体是否相等,比如,

package main
import "fmt"
type User struct {
  Name string
  Age  int
}
func main() {
  user1 := User{}
  user2 := User{}
  fmt.Println(user1 == user2)
  // true
}

这样是没问题的,如果我往结构体加一个这样的字段呢?

package main
import "fmt"
type User struct {
  Name string
  Age  int
  IsChild func(age int) bool
}
func main() {
  user1 := User{}
  user2 := User{}
  fmt.Println(user1 == user2)
  // invalid operation: user1 == user2 (struct containing func(int) bool cannot be compared)
}

直接报编译错误,为什么?


如果结构体中的任一字段不具有可比性,那么使用 "==" 运算符会导致编译出错。上面我加的 IsChild 字段类型为闭包函数显然不具备可比性。


groutine


package main
import (
  "fmt"
)
func main() {
  var hi string
  go func() {
    hi = "golang"
  }()
  fmt.Println(hi)
}

以上输出什么?


大概率啥都没有,因为在子协程给 hi 变量赋值前,主协程大概率先打印,然后运行结束,接着整个程序结束了。


用最愚蠢的方法让主协程停一下。

package main
import (
  "fmt"
  "time"
)
func main() {
  var hi string
  go func() {
    hi = "golang"
  }()
  time.Sleep(10 * time.Millisecond)
  fmt.Println(hi)
  //golang
}

再来看这题,

package main
import (
  "fmt"
  "sync"
)
func main() {
  var wg sync.WaitGroup
  for i := 0; i < 5; i++ {
    wg.Add(1)
    go func() {
      fmt.Println("值是:", i)
      wg.Done()
    }()
  }
  wg.Wait()
}


每次运行,答案都不相同,但是大概率都是5。这里的操作存在数据竞争,即 data race。这种情况发生的条件是,当两个或两个以上 groutine 并发地访问同一个变量并且有一个访问是写入时,就会引发 data race。


可以通过命令行新增参数 -race 运行检测。

1668495306405.jpg

修复的方式也会简单,在启动 groutine 时使用局部变量并将数字作为参数传递。

package main
import (
  "fmt"
  "sync"
)
func main() {
  var wg sync.WaitGroup
  for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(item int) {
      fmt.Println("值是:", item)
      wg.Done()
    }(i)
  }
  wg.Wait()
}

 

recover()


在 go 中可以使用 recover() 函数捕获 panic,但是我们也需要注意它的用法,以下使用姿势都是错误的。

package main
import "fmt"
func main() {
  recover()
  panic("make error")
}
// 错误姿势


package main
import "fmt"
func main() {
  doRecover()
  panic("make error")
}
func doRecover() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println("出错了")
    }
  }()
}
// 错误姿势
package main
import "fmt"
func main() {
  defer func() {
    defer func() {
      if err := recover(); err != nil {
        fmt.Println(err)
      }
    }()
  }()
  panic("make error")
}
// 错误姿势


它只有在延迟函数中直接调用才能生效。

package main
import "fmt"
func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println(err)
    }
  }()
  panic("make error")
}
// 正确姿势

还有好多错误姿势没有列举,你有不一样的错误操作嘛?欢迎下方留言一起讨论。

相关文章
|
12月前
|
Go C语言
GO基础知识分享2
GO基础知识分享2
|
6月前
|
存储 Java Go
「刷起来」Go必看的进阶面试题详解
「刷起来」Go必看的进阶面试题详解
Go好玩的面试题之回文判断
回文,汉语词语,指汉语中的回文语法,即把相同的词汇或句子,在下文中调换位置或颠倒过来,产生首尾回环的情况,叫做回文,也叫回环。
152 0
Go好玩的面试题之回文判断
|
程序员 Go 开发工具
0429 go教程 菜鸟
感觉菜鸟教程就是简单的将go语言的各知识点做一些简单的介绍,罗列,并没有串联起来,具体细节也不是很全,当然对于我这种菜鸟来说的话,感觉也还是挺不错的,在这里也给菜鸟的维护人员(据我所知好像就创始人一个人?)点赞以示感谢
213 0
|
架构师 Java 程序员
Go 学习路线(2022) (一)
Go 学习路线(2022) (一)
521 0
Go 学习路线(2022) (一)
|
开发框架 缓存 NoSQL
Go 学习路线(2022) (二)
Go 学习路线(2022) (二)
795 0
Go 学习路线(2022) (二)
|
编译器 Go 开发者
Go 专栏|说说方法
Go 专栏|说说方法
106 0
Go 专栏|说说方法
|
Go
Go 专栏|函数那些事
Go 专栏|函数那些事
133 0
Go 专栏|函数那些事