一、For & For-Range
循环的两种方式:for 、 for range
package main
import "fmt"
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
// 初始化语句和后置语句是可选的。
sum := 1
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum)
// for 是 Go 语法内的 while
for sum < 1000 {
sum += sum
}
// for-range
numbers := [6]int{1, 2, 3, 5}
for i,x:= range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}
}
1-1 range 循环数组返回索引和值
package main
import "fmt"
func main() {
x := []string{"a","b","c"}
for _, v := range x {
fmt.Println(v) //prints a, b, c
}
}
1-2 slice、array、map 在 range 中传递方式 - 通常为值传递
在循环的通常情况下,仅传递值,不对原内存地址的值进行修改
package main
import "fmt"
func main() {
data := []int{1,2,3}
for _,v := range data {
v *= 10 // original item is not changed
}
fmt.Println("data:",data) // prints data: [1 2 3]
}
使用索引操作符来修改值,则会将原内存地址的值修改
package main
import "fmt"
func main() {
data := []int{1,2,3}
for i,_ := range data {
data[i] *= 10
}
fmt.Println("data:",data) //prints data: [10 20 30]
}
若变量内部存储的为指针,索引和range 出的值都可对原地址进行修改
package main
import "fmt"
func main() {
data := []*struct{num int} { {1},{2},{3} }
for _,v := range data {
v.num *= 10
}
fmt.Println(data) //[0xc0000a6008 0xc0000a6010 0xc0000a6018]
fmt.Println(data[0],data[1],data[2]) //&{10} &{20} &{30}
fmt.Println(*data[0],*data[1],*data[2]) //{10} {20} {30}
}
1-3 for 循环中的返回值为内存地址
1-3-1 for 循环的 goroutine 存在数据错误
for 循环中迭代的变量,都是指向同一个内存地址,即 for 循环中创建的闭包都为引用传递,而由于 goroutine 本身的执行速度,很可能主线程已经循环结束,子线程才开始进行执行,导致数据获取的已经不是想象中的那个值。
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func() {
fmt.Println(v)
}()
}
time.Sleep(3 * time.Second)
}
/*
three
three
three
*/
然而,若主线程中存在io等待拖慢速度,会发现 子线程仍然会获取到想象中的值。
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func() {
fmt.Println(v)
}()
time.Sleep(3 * time.Second)
}
time.Sleep(3 * time.Second)
}
/*
one
two
three
*/
解决方式:1、保存每次迭代的值 2、每次迭代的值是匿名 goroutine 的参数
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
vcopy := v // 保存每次迭代的值,值传递,仅仅复制值
go func() {
fmt.Println(vcopy)
}()
}
time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}
ackage main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func(in string) {
fmt.Println(in)
}(v) // 作为参数传入执行
}
time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}
注意:若循环的对象是指针,则每次取的地址都不同,则不存在指向错误。
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data {
fmt.Printf("----->%v\n", v)
go v.print()
}
time.Sleep(3 * time.Second)
}
/*
----->&{one}
----->&{two}
----->&{three}
three
two
one
*/
1-3-2 由于指向内存地址导致的匿名函数作用域错误
import (
"fmt"
)
func main(){
var msgs []func()
array := []string{
"1", "2", "3", "4",
}
for _, e := range array{
msgs = append(msgs, func(){
fmt.Println(e)
})
}
for _, v := range msgs{
v()
}
}
/*
4
4
4
4
*/
// 由于 e 指向的是内存地址,每次循环内存地址内保存的值会被修改,所以出错
1-4 可以边修改边循环,遍历不会获取修改数据 - 循环前拷贝
v := []int{1, 2, 3}
for i := range v {
v = append(v, i)
}
1-4-1 map 的遍历中修改
map 的内部实现是一个链式 hash 表,为保证每次的无序,初始化时,都会随机遍历一个开始的位置。且,once.Do 函数用于保护已遍历的数据,如果后续删除的元素在之前没有被遍历到,则后续遍历不会出现。
var m = map[int]int{1: 1, 2: 2, 3: 3}
//only del key once, and not del the current iteration key
var o sync.Once
for i := range m {
o.Do(func() {
for _, key := range []int{1, 2, 3} {
if key != i {
fmt.Printf("when iteration key %d, del key %d\n", i, key)
delete(m, key)
break
}
}
})
fmt.Printf("%d%d ", i, m[i])
}
1-4-1-1 遍历中删除未遍历到的map元素,后续不会遍历到已经删除的元素
//遍历中删除map元素,不会遍历到已经删除的元素
package main
import "fmt"
func main() {
var createElemDuringIterMap = func() {
var m = map[int]int{1: 1, 2: 2, 3: 3}
for i := range m {
delete(m, 2)
fmt.Printf("%d%d ", i, m[i])
}
}
for i := 0; i < 10; i++ {
// will not show 22
createElemDuringIterMap()
fmt.Println()
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-821eIF7f-1585530422471)(Go-Keywords.assets/image-20200313152016305.png)]
1-4-1-2 遍历中删除已遍历到的元素,后续遍历 vlaue 为空
package main
import "fmt"
func main() {
var createElemDuringIterMap = func() {
var m = map[int]int{1: 1, 2: 2, 3: 3}
for i := range m {
delete(m, 1)
fmt.Printf("%d%d ", i, m[i])
}
}
for i := 0; i < 10; i++ {
// will not show 22
createElemDuringIterMap()
fmt.Println()
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CtburoDz-1585530422472)(Go-Keywords.assets/image-20200313152725507.png)]
1-4-1-3 遍历中新增元素,可能会遍历到后增的元素。
// 遍历中新增元素,可能会遍历到后增的元素。
package main
import "fmt"
func main() {
var createElemDuringIterMap = func() {
var m = map[int]int{1: 1, 2: 2, 3: 3}
for i := range m {
m[4] = 4
fmt.Printf("%d%d ", i, m[i])
}
}
for i := 0; i < 10; i++ {
//some line will not show 44, some line will
createElemDuringIterMap()
fmt.Println()
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8gCePLy-1585530422473)(Go-Keywords.assets/image-20200313152105372.png)]
1-5 遍历大数据前的拷贝造成的内存浪费优化
package main
import (
"fmt"
)
func main(){
arr := [...]int{102400:0}
for i, n := range arr {
//just ignore i and n for simplify the example
_ = i
_ = n
}
}
优化的两种方式:
- 取址遍历 for i, n := range &arr
- 对数组做切片引用 for i, n := range arr[:]
arr := [...]int{102400:0}
for i, n := range &arr {
// do anything
}
二、Select
select 是 Go 中的一个控制结构,类似于用于 channel 的 switch 语句。每个 case 必须是一个 channel 操作,要么是发送要么是接收。
select 能让 goroutine 同事等待 多个 channel 的可读可写。
当满足多个 case 时,select 随机执行一个可运行的 case。
如果没有 case 可运行,它将阻塞,直到有 case 可运行。
一个默认的子句应该总是可运行的。
select {
case i := <-c:
// 使用 i
default:
// 从 c 中接收会阻塞时执行
}
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
/*
0
1
1
2
3
5
8
13
21
34
quit
*/
三、Defer
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
package main
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
3-1 Defer 在当前函数末尾调用执行
被 defer 调用是在包含 defer 的函数末尾,而非同个代码块的末尾。
如果在一个长时间运行的函数内的一个循环内调用 defer 用于清理资源,则会出错。
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
if len(os.Args) != 2 {
os.Exit(-1)
}
start, err := os.Stat(os.Args[1])
if err != nil || !start.IsDir(){
os.Exit(-1)
}
var targets []string
filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.Mode().IsRegular() {
return nil
}
targets = append(targets,fpath)
return nil
})
for _,target := range targets {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err) //prints error: too many open files
break
}
defer f.Close()
//will not be closed at the end of this code block
//do something with the file...
}
}
解决方式:将需要清理资源的代码,包装成函数,在函数内部调用 defer。
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
if len(os.Args) != 2 {
os.Exit(-1)
}
start, err := os.Stat(os.Args[1])
if err != nil || !start.IsDir(){
os.Exit(-1)
}
var targets []string
filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.Mode().IsRegular() {
return nil
}
targets = append(targets,fpath)
return nil
})
for _,target := range targets {
func() {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err)
return
}
defer f.Close() //ok
//do something with the file...
}()
}
}
3-2 Defer 先传值后调用
Defer 的函数,传入的参数会立刻拷贝求值,直到外层函数返回该函数前都不会被调用。
package main
import "fmt"
func main() {
a := 1
defer fmt.Printf("defer %v\n", a)
a = 2;
fmt.Printf("main %v\n", a)
}
/*
main 2
defer 1
*/
解决方式:向 defer 传入匿名函数
因为拷贝的是函数指针,所以会在外层函数执行完毕在计算参数
package main
import "fmt"
func main() {
a := 1
defer func() { fmt.Printf("defer %v\n", a) }()
a = 2
fmt.Printf("main %v\n", a)
}
/*
main 2
defer 2
*/
3-2-1 defer 求值抛错
由于先求值,但传入的参数为空等情况,会导致抛错。
type Test struct {
Max int
}
func (t *Test) Println() {
fmt.Println(t.Max)
}
func deferExec(f func()) {
f()
}
func call() {
var t *Test
// 求值时,t 为 nil
defer deferExec(t.Println)
t = new(Test)
}
3-3 多个 defer 的执行顺序是 LIFO(先进后出)
package main
import "fmt"
func main(){
defer func(){
fmt.Println("1")
}()
defer func(){
fmt.Println("2")
}()
defer func(){
fmt.Println("3")
}()
}
/*
3
2
1
*/
四、Panic & Recover
- Panic: 能够改变程序的控制流,函数调用
panic
时会立刻停止执行函数的其他代码,并在执行结束后在当前 Goroutine 中递归执行调用方的延迟函数调用defer
; - Recover: 可以中止
panic
造成的程序崩溃。它是一个只能在defer
中发挥作用的函数,在其他作用域中调用不会发挥任何作用;
注意:
panic
只会触发当前 Goroutine 的延迟函数调用;recover
只有在defer
函数中调用才会生效;panic
允许在defer
中嵌套多次调用;
package main
import "fmt"
func main() {
defer func() {
fmt.Println("recovered:",recover())
}()
panic("not good")
}
4-1 Recover 只能在 Defer 一层包裹下调用
// recover 不可在其他函数中包裹调用
package main
import "fmt"
func doRecover() {
fmt.Println("recovered =>",recover()) //prints: recovered => <nil>
}
func main() {
defer func() {
doRecover() //panic is not recovered
}()
panic("not good")
}
4-2 Panic 只触发当前 Goroutine 的 Defer 调用 - 跨协程失效
多个 Goroutine 之间没有太多的关联,一个 Goroutine 在 panic
时也不应该执行其他 Goroutine 的延迟函数。
package main
import (
"time"
)
func main() {
defer println("in main")
go func() {
defer println("in goroutine")
panic("")
}()
time.Sleep(1 * time.Second)
}
// main 函数中的 defer 语句并没有执行,执行的只有当前 Goroutine 中的 defer。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P5aqJh4p-1585530422473)(Go-Keywords.assets/image-20200313160302172.png)]
4-3 Panic 可嵌套调用
package main
import (
"fmt"
)
func main() {
defer fmt.Println("in main")
defer func() {
defer func() {
panic("panic again and again")
}()
panic("panic again")
}()
panic("panic once")
}
// 注意 Defer 的触发顺序(LIFO)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0MZl2SaV-1585530422474)(Go-Keywords.assets/image-20200313160752563.png)]
4-4 VS Errors Interface
通常情况下往往使用 Errors 接口用于异常情况的处理
只有在程序无法在异常情况下继续正常执行的时候,使用 panic 来终止程序,再使用 recover 恢复程序,
一般情况下,应该避免使用 panic 和 recover,尽可能使用 errors 替代。
4-4-1 使用 panic 的应用场景
- 不可恢复的错误,让程序不能继续进行。
比如说Web服务器无法绑定到指定端口。在这种情况下,panic是合理的,因为如果端口绑定失败接下来的逻辑继续也是没有意义的。 - coder的人为错误
假设我们有一个接受指针作为参数的方法,然而使用了nil作为参数调用此方法。在这种情况下,我们可以用panic,因为该方法需要一个有效的指针
4-4-2 Errors Interface
type error interface {
Error() string
}
type networkProblem struct {
message string
code int
}
func (np networkProblem) Error() string {
return fmt.Sprintf("network error! message: %s, code: %v", np.message, np.code)
}
func main() {
np := networkProblem{
message: "we received a problem",
code: 404,
}
fmt.Println(np.Error())
// prints "network error! message: we received a problem, code: 404"
}
五、Make & New
当我们想要在 Go 语言中初始化一个结构时,可能会用到两个不同的关键字 — make
和 new
。
make
的作用是初始化内置的数据结构,即变量的内部机构:切片、哈希表和 Channel;new
的作用是根据传入的类型在堆上分配一片内存空间并返回指向这片内存空间的指针;
5-1 初始化时的栈和堆变量
Go 中,使用 new() 或者 make() 创建变量,变量的位置(栈、堆)是由编译器决定的。编译器根据变量的大小和泄露分析的结果来决定分配的位置。这意味着,在局部变量上返回引用是没有问题的。
// 获取变量分配的位置
// If you need to know where your variables are allocated pass the "-m" gc flag to "go build" or "go run"
go run -gcflags -m app.go