Go-关键词总结

本文详细探讨了 Go 语言中的核心关键词,包括 For 和 For-Range 的使用,如 range 循环数组、map 的遍历及其注意事项;Select 的工作原理,如何在并发环境中使用;Defer 的执行顺序和常见误区;Panic 和 Recover 的交互,以及何时使用它们;最后,对比了 Make 和 New 的区别以及初始化变量在栈和堆的情况。
摘要由CSDN通过智能技术生成

一、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 
}
}

优化的两种方式:

  1. 取址遍历 for i, n := range &arr
  2. 对数组做切片引用 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 的应用场景

  1. 不可恢复的错误,让程序不能继续进行。
    比如说Web服务器无法绑定到指定端口。在这种情况下,panic是合理的,因为如果端口绑定失败接下来的逻辑继续也是没有意义的。
  2. 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 语言中初始化一个结构时,可能会用到两个不同的关键字 — makenew

  • make 的作用是初始化内置的数据结构,即变量的内部机构:切片、哈希表和 Channel;
  • new 的作用是根据传入的类型在堆上分配一片内存空间并返回指向这片内存空间的指针;

golang-make-and-ne

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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值