您的位置:首页 > 编程语言 > Go语言

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

2017-10-22 00:00 423 查看
摘要: 变量声明和生命周期、简单的逃逸分析、指针的作用和flag标准库的简单使用

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

下一篇 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, 所以初始化零值的策略也有所不同。

简单的逃逸分析

先来个DEMO

var p= f()

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

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

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

指针的价值

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

标准库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
, 则自动换行。

变量的生命周期

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

对于包级别的变量来说,它们的生命周期和整个程序的运行周期是一致的;

对于局部变量的生命周期则是动态的: 从每次创建一个变量开始,直到这个变量不再被任何别名引用为止,然后内存空间才会被回收

函数的输入参数和输出参数都是局部变量,它们在函数每次被调用的时候创建,调用完成则释放。

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

一个变量的生命周期只取决于是否可达,因此局部变量的生命周期可能会超出其作用域,所以局部变量可能在函数返回之后依然存在。编译器会自动选择变量是分配在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变量没有使用”的错误。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: