Go圣经-学习笔记之程序结构

上一篇 Go圣经-学习笔记入门bufio.Writergolang

下一篇 Go圣经-学习笔记之程序结构(二)数组

变量声明

它包括四种类型:const常量,type类型,func函数和var变量。函数

var 变量名字 类型= 表达式

若是去掉= 表达式, 则Go语言将使用零值初始化该变量。数值类型变量对应的值的0,bool类型变量对应的零值是false,字符串类型变量对应的零值是空字符串,接口或者引用类型(包括slice、map、chan和函数)变量对应的零值是nil。数组或者结构体变量对应的零值是各个元素对应的零值。因此,go语言中不存在没有初始化的类型变量学习

这里要讨论一个问题:咱们知道slice类型实际底层是struct结构体类型: struct { byte *array; uintgo len; uintgo cap; }; 理应slice零值是各个元素的初始化零值,实际上slice在Go语言内部把slice当作了一种新的类型。slices类型:types.TSLICE和struct类型:types.TSTRUCT, 因此初始化零值的策略也有所不一样。ui

简单的逃逸分析

先来个DEMO.net

var p= f()

func f() *int{
    v:=1
    return &v
}

这段程序跑起来不会发生段错误,非法地址。知道其余主流语言的开发人员,可能会认为v变量是在stack上分配的,函数执行完成,则函数的stack被释放到,这是返回值非法引用v变量,则致使程序崩溃。指针

可是Go语言编译器会静态分析源码,它发现f函数外部有引用局部变量v。则v在函数内的内存空间分配是在heap上分配,使得p可使用它,这就是一个简单的逃逸分析。你们能够网上查查编译器的逃逸分析。至于内存的释放是由Go语言提供的自动垃圾回收机制作的,不须要人工参与。code

指针的价值

指针特别有价值的地方在于,咱们能够不用知道变量的名字而访问一个变量。言外之意,只要你给我一块内存地址,给我这个这块内存地址块的数据类型,我就能够正确访问原来这个变量值。1.经过内存地址,则能够知道数据的起始位置;2.经过数据类型,则能够知道,简单数据类型或者复杂数据类型中的各个元素所占内存空间的大小,因此,咱们就能够经过指针正确访问内存变量值。可是一个缺点在于:在垃圾回收时,要找到一个变量的全部访问者并不容易,咱们必须知道全部变量所有的别名。只有在全部变量不在使用这块内存时,咱们才能回收内存。blog

标准库flag简单使用

借助于上面的指针理解,咱们如今分析下flag标准库的部分使用。接口

可能有些开发者常常会看到形以下面的使用,但不怎么会使用:

var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("sep", " ", "separator")

func main(){
    flag.Parse()
    fmt.Print(strings.Join(flag.Args(), *sep))
    if !*n{
        fmt.Println()
    }
}

有童鞋可能想问,为啥n和sep没有任何赋值操做, main函数就能够直接使用这两个指针变量了呢?由于这个程序在运行时,有一个全局变量CommandLine, 当flag.Parse解析os.Stdin的全部输入参数时,把CommandLine的指针变量n和sep所有赋值,同时经过flag.Args方法,能够获取到用户输入的数据列表。明白了这个,就知道怎么使用flag包了。

注意一点:对于bool变量,只须要:./program -n -sep , hello world , 输出: hello,world,且不换行。若是去掉-n, 则自动换行。

变量的生命周期

变量的生命周期是指在程序运行期间变量有效存在的时间间隔。

  1. 对于包级别的变量来讲,它们的生命周期和整个程序的运行周期是一致的;
  2. 对于局部变量的生命周期则是动态的: 从每次建立一个变量开始,直到这个变量再也不被任何别名引用为止,而后内存空间才会被回收
  3. 函数的输入参数和输出参数都是局部变量,它们在函数每次被调用的时候建立,调用完成则释放。

垃圾收集器的基本思路:从每一个包级别和每一个当前运行函数的局部变量开始,经过指针或者引用的方式遍历路径,是否能够找到该变量。若是找不到,则变量不可达。

一个变量的生命周期只取决因而否可达,所以局部变量的生命周期可能会超出其做用域,因此局部变量可能在函数返回以后依然存在。编译器会自动选择变量是分配在heap上仍是stack上。这个选择不是由var或者new决定的

var global *int

func f() { // 由于global引用了x,因此x变量逃逸了,分配在heap上
    var x int
    x = 1
    global = &x
}

func g() { // 由于外部没有引用y,y不可达,分配在stack上。
    y := new(int)
    *y = 1
}

元组赋值

x, y = y, x+y

在赋值以前,赋值语句右边的全部表达式将会先进行求值,而后再统一更新左边对应变量的值

做用域

当编译器遇到一个名字引用时,若是它看起来像一个声明,它首先从最内层的词法域向全局的做用域查找。若是查找失败,则报告“未声明的变量名”错误。若是该变量名在内部和外部都声明过,这内部块的声明首先被找到。内部声明屏蔽了外部声明。

说明:做用域和变量的生命周期是两个不一样概念,前者是编译时的一个静态属性,后者是程序运行过程当中变量存在的有效时间段,它是一个运行时概念。

一个隐晦的bug

var cwd string
func assignCwd() {
    cwd, err := os.Getwd()
    if err !=nil{
        log.Fatalf("os.Getwd failed. %v", err)
        return
    }
    fmt.Println(cwd)
    return
}
func main(){
    assignCwd()
    fmt.Println(cwd)
}

这个程序的答案:通常初学者认为cwd不为空。由于他们遇到过这样的DEMO

func readLine() {
    var data []byte
    data, err := ioutil.ReadAll(filename) // 这个data是赋值操做,err是声明操做。
    if err !=nil{
        log.Fatalf(err)
        return
    }
}

因此认为assignCwd函数的cwd是赋值操做,因此cwd不为空字符串。实际上后面这个DEMO的词法域都在一块儿,而前者cwd分别是包级别和函数块级别的变量。内部声明的cwd屏蔽了外部的变量,因此致使隐晦bug。若是在assignCwd函数内不打印cwd变量,则直接报“cwd变量没有使用”的错误。

相关文章
相关标签/搜索