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 运行检测。
修复的方式也会简单,在启动 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") } // 正确姿势
还有好多错误姿势没有列举,你有不一样的错误操作嘛?欢迎下方留言一起讨论。