变量

1.声明变量

使用var关键字可以创建一个指定类型的变量:

var i int = 0
var i = 0
var i int

以上三个表达式均是合法的,第三个表达式会将i初始化为int类型的零值,0;如果i是bool类型,则为false;i是float64类型,则为0.0;i为string类型,则为"";i为interface类型,则为nil;i为引用类型,则为nil;如果i是struct,则是将struct中所有的字段初始化为对应类型的零值。

这种初始化机制可以保证任何一个变量都是有初始值的,这样在做边界条件条件检查时不需要担心值未初始化,可以避免一些潜在的错误,相信C和C++程序员的体会更加深入。
var s string
fmt.Println(s) // ""

这里的s是可以正常打印的,而不是导致某种不可预期的错误。

可以在同一条语句中声明多个变量:

var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string

包内可见的变量在main函数开始执行之前初始化,本地变量在函数执行到对应的声明语句时初始化。

变量也可以通过函数的返回值来初始化:

var f, err = os.Open(name) // os.Open returns a file and an error

2.短声明

在函数内部,有一种短声明的方式,形式是name := expression,这里,变量的类型是由编译器自动确定的。

anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0

因为这种形式非常简洁,因此在函数内部(本地变量)大量使用。如果需要为本地变量显式的指定类型,或者先声明一个变量后面再赋值,那么应该使用var:

i := 100                  // an int
var boiling float64 = 100 // a float64var names []string
var err error
var p Point

就像var声明一样,短声明也可以并行初始化,

i, j := 0, 1

要谨记的是,:=是一个声明,=是一个赋值,因此在需要赋值的场所不能使用 :=

var i int
i := 10//panic : no new variables on left side of :=

可以利用并行赋值的特性来进行值交换:

i, j = j, i // swap values of i and j

有一点需要注意的:短声明左边的变量未必都是新声明的!

in, err := os.Open(path1) //新声明两个变量:in, err
//...
out, err := os.Create(path2)
/*因为err已经声明过,因此这里只新声明一个变量out。
虽然这里使用:=,但是err是在上个语句声明的,这里仅仅是赋值*/

而且,短声明的左边变量必须有一个是新的,若都是之前声明过的,会报编译错误:

f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables

正确的写法是这样的:

f, err := os.Open(infile)
// ...
f, err = os.Create(outfile) // compile ok

3.指针

值变量的存储地址存的是一个值。例如 x = 1 就是在x的存储地址存上1这个值; x[i] = 1 代表在数组第i + 1的位置存上1这个值;x.f = 1,代表struct x中的f字段所在的存储位置存上1这个值。

指针值是一个变量的存储地址。注意:不是所有的值都有地址,但是变量肯定是有地址的!这个概念一定要搞清楚! 通过指针,我们可以间接的去访问一个变量,甚至不需要知道变量名。

var x int = 10
p := &x
/*&x是取x变量的地址,因此p是一个指针,指向x变量.
这里p的类型是*int,意思是指向int的指针*/
fmt.Printf("addr:%p, value:%d\n", p, *p)
//output: addr:0xc820074d98, value:10
*p = 20 // 更新x到20

上面的代码中,我们说p指向x或者p包含了x的地址。*p的意思是从p地址中取出对应的变量值,因此*p就是x的值:10。因为*p是一个变量,因此可以作为左值使用,*p = 20,这时代表p地址中的值更新为20,因此这里x会变为20。下面的例子也充分解释了指针的作用:

x := 1
p := &x         // p类型:*int,指向x
fmt.Println(*p) // "1"
*p = 2          // 等价于x = 2
fmt.Println(x)  // "2"

聚合类型struct或者array中的元素也是变量,因此是可以通过寻址(&)获取指针的。

若一个值是变量,那么它就是可寻址的,因此若一个表达式可以作为一个变量使用时,意味着该表达式可以寻址,也可以被使用&操作符。

` 指针的零值是nil(记得之前的内容吗?go的所有类型在没有初始值时都默认会初始化为该类型的零值)。若p指向一个变量,那么p != nil 就是true,因为p会被赋予变量的地址。指针是可以比较的,两个指针相等意味着两个指针都指向同一个变量或者两个指针都为nil。

var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"    

在函数中返回一个本地变量的地址是很安全的。例如以下代码,本地变量v是在f中创建的,从f返回后依然会存在,指针p仍然会去引用v

var p = f()
fmt.Println(*p) //output:1
func f() *int {v := 1return &v
}

每次调用f都会返回不同的指针,因为f会创建新的本地变量并返回指针:

fmt.Println(f() == f()) // "false"

把变量的指针传递给函数,即可以在函数内部修改该变量(go的函数默认是值传递,所有的值类型都会进行内存拷贝)

func incr(p *int) int {*p++ // increments what p points to; does not change preturn *p
}v := 1
incr(&v)              // v现在是2
fmt.Println(incr(&v)) // "3" (and v is 3)

指针在flag包中是很重要的。flag会读取程序命令行的参数,然后设置程序内部的变量。下面的例子中,我们有两个命令行参数:-n,不打印换行符;-s sep,使用自定义的字符串分隔符进行打印.

package mainimport ("flag""fmt""strings"
)var n = flag.Bool("n", false, "忽略换行符")
var sep = flag.String("s", " ", "分隔符")func main() {flag.Parse()fmt.Print(strings.Join(flag.Args(), *sep))if !*n {fmt.Println()}
}

flag.Bool会创建一个bool类型的flag变量,flag.Bool有三个参数:flag的名字,命令行没有传值时默认的flag值(false),flag的描述信息( 当用户传入一个非法的参数或者-h、 -help时,会打印该描述信息)。变量sep和n 都是flag变量的指针,因此要通过*sep和*n来访问原始的flag值。

当程序运行时,在使用flag值之前首先要调用flag.Parse。非flag参数可以通过args := flag.Args()来访问,args的类型是[]string(见后续章节)。如果flag.Parse报错,那么程序就会打印出一个使用说明,然后调用os.Exit(2)来结束。

让我们来测试一下上面的程序:

$ go build gopl.io/ch2/echo4
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -n a bc def
a bc def$
$ ./echo4 -help
Usage of ./echo4:-n    忽略换行符-s string分隔符 (default " ")

4.new函数

还可以通过内建(built-in)函数new来创建变量。new(T)会初始化一个类型为T的变量,值为类型T对应的零值,然后返回一个指针:*T。

p := new(int)   // p,类型*int,指向一个没有命名的int变量
fmt.Println(*p) // "0"
*p = 2
fmt.Println(*p) // "2"

这种声明方式和普通的var声明再取地址没有区别。如果你不想绞尽脑汁的去思考一个变量名,那么就可以使用new:

func newInt() *int {            func newInt() *int {return new(int)                 var dummy int
}                                   return &dummy}

每次调用new都会返回一个唯一的地址

p := new(int)
q := new(int)
fmt.Println(p == q) // "false"

但是有一个例外:比如struct{}或[0]int,这种类型的变量没有包含什么信息且为零值,可能会有同样的地址。

new函数相对来说是较少使用的,因为最常用的未具名变量是struct类型,对于这种类型而言,相应的struct语法更灵活也更适合。

因为new是预定义的函数名(参见上一节的保留字),不是语言关键字,因此可以用new做函数内的变量名:

func delta(old, new int) int { return new - old }

当然,在delta函数内部,是不能再使用new函数了!

5.变量的生命期

变量的生命期就是程序执行期间变量的存活期。包内可见的变量的生命期是固定的:程序的整个执行期。作为对比,本地变量的生命期是动态的:每次声明语句执行时,都会创建一个新的变量实例,变量的生命期就是从创建到不可到达状态(见下文)之间的时间段,生命期结束后变量可能会被回收。

函数的参数和本地变量都是动态生命期,在每次函数调用和执行的时候,这些变量会被创建。例如下面的代码:

for t := 0.0; t < cycles*2*math.Pi; t += res {x := math.Sin(t)y := math.Sin(t*freq + phase)img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),blackIndex)}

每次for循环执行时,t,x,y都会被重新创建。

那么GC是怎么判断一个变量应该被回收呢?完整的机制是很复杂的,但是基本的思想是寻找每个变量的过程路径,如果找不到这样的路径,那么变量就是不可到达的,因此就是可以被回收的。

一个变量的生命期只取决于变量是否是可到达的,因此一个本地变量可以在循环之外依然存活,甚至可以在函数return后依然存活。编译器会选择在堆上或者栈上去分配变量,但是请记住:编译器的选择并不是由var或者new这样的声明方式决定的。

var global *intfunc f() {                      func g() {var x int                       y := new(int)x = 1                           *y = 1global = &x                 }
}

上面代码中,x是在堆上分配的变量,因为在f返回后,x也是可到达的(global指针)。这里x是f的本地变量,因此,这里我们说x从f中逃逸了。相反,当g返回时,变量*y就变为不可到达的,然后会被垃圾回收。因为*y没有从g中逃逸,所以编译器将*y分配在栈上(即使是用new分配的)。在绝大多数情况下,我们都不用担心变量逃逸的问题,只要在做性能优化时意识到:每一个逃逸的变量都需要进行一次额外的内存分配。

尽管自动GC对于写现代化的程序来说,是一个巨大的帮助,但是我们也要理解go语言的内存机制。程序不需要显式的内存分配或者回收,可是为了写出高效的程序,我们仍然需要清楚的知道变量的生命期。例如,在长期对象(特别是全局变量)中持有指向短期对象的指针,会阻止GC回收这些短期对象,因为在这种情况下,短期对象是可以到达的!!

文章所有权:Golang隐修会 联系人:孙飞,CTO@188.com!

Go语言核心之美 1.2-变量及声明篇相关推荐

  1. Go语言核心之美-必读

    Go语言核心之美开篇了!,无论你是新手还是一代高人,在这个系列文章中,总能找到你想要的! 博主是计算机领域资深专家并且是英语专8水平,翻译标准只有三个:精确.专业.不晦涩,为此每篇文章可能都要耗费数个 ...

  2. java for循环定义变量,在java语言里for循环里的变量如何声明在外面进行使用。

    在java语言里for循环里的变量如何声明在外面进行使用. 关注:142  答案:2  手机版 解决时间 2021-02-01 21:59 提问者懷念那年夏天 2021-01-31 21:11 pub ...

  3. Go语言核心之美 3.3-Map

    哈希表是一种非常好用.适用面很广的数据结构,是key-value对的无序集合.它的key是唯一的,通过key可以在常数复杂度时间内进行查询.更新或删除,无论哈希表有多大. Go语言的map类型就是对哈 ...

  4. Go语言核心之美 1.5-作用域

    变量的作用域是指程序代码中可以有效使用这个变量的范围.不要将作用域和生命期混在一起.作用域是代码中的一块区域,是一个编译期的属性:生命期是程序运行期间变量存活的时间段,在此时间段内,变量可以被程序的其 ...

  5. Go语言核心之美 3.4-Struct结构体

    struct(结构体)也是一种聚合的数据类型,struct可以包含多个任意类型的值,这些值被称为struct的字段.用来演示struct的一个经典案例就是雇员信息,每条雇员信息包含:员工编号,姓名,住 ...

  6. Go语言核心之美 2.6-常量

    在Go语言中,常量表达式是在编译期求值的,因此在程序运行时是没有性能损耗的.常量的底层类型是前面提过的基本类型:布尔值,字符串,数值变量. 常量的声明方式和变量很相似,但是常量的值是不可变的,因此在运 ...

  7. Go语言核心之美 1.4-包和文件

    一.Package Go语言中的包(Package)就像其它语言的库(Library)或模块(Module)一样,支持模块化,封装性,可重用性,单独编译等特点.包的源码是由数个.go文件组成,这些文件 ...

  8. Go语言核心之美 2.5-字符串

    字符串是不可变的字节序列,虽然可以包含任意数据,包括0这个字节,不过字符串通常是用来包含可读性较强的文本.文本字符串通常采用UTF-8编码,由Unicode码点(rune)组成. 内置的len函数会返 ...

  9. Go语言核心之美 3.2-slice切片

    Slice(切片)是长度可变的元素序列(与之对应,上一节中的数组是不可变的),每个元素都有相同的类型.slice类型写作[]T,T是元素类型.slice和数组写法很像,区别在于slice没有指定长度. ...

最新文章

  1. 启动脚本gameserver
  2. 视图的getWidth()和getHeight()返回0
  3. ISC 2020技术日丨 网络空间危机四伏,如何发现威胁的蛛丝马迹?
  4. es中的AllocationService
  5. jvm性能调优实战 - 48无限循环调用和没有缓存的动态代理引起的OOM
  6. Tomcat设置虚拟目录的方法, 不修改server.xm
  7. package-lock.json是做什么用的_做鱼缸用什么玻璃好?
  8. 2019.4.27 人工智能培训安装工作记录
  9. php两个数组融合,php合并两个数组的方式有哪些
  10. 半年全球网络安全入侵事件近千起,超19亿数据受影响
  11. c java socket编程_java+swing C/s模式的socket编程与长短连接
  12. 一行.bat代码实现win+L锁定计算机立即锁屏
  13. mysql数据库输入窗体vbs代码_VBS教程:VBScript 与窗体
  14. 数学建模-多元线性回归
  15. 安利——程序猿必备笔记软件typora+坚果云
  16. 让你百分百玩转抖音!
  17. springboot +mybatis实现多表一对一查询
  18. Qt QVector “isDetached()“
  19. keras进阶之poly学习率
  20. 过年的气氛为什么几乎全无,内心也没有任何期盼呢?

热门文章

  1. Axure实战——实现登录注册功能
  2. CS1703 C# Multiple assemblies with equivalent xxx... and. Remove one of the duplicate references.
  3. 国科大学习资料--人工智能原理与算法-第十次作业解析(学长整理)
  4. MaaS无缝出行服务呼之欲出 传统出行模式将被颠覆
  5. 季冠携“闪星服务”受邀参加2021连锁企业轻资产论坛
  6. php 国际标准时间_关于时区:PHP date_default_timezone_set()东部标准时间(EST)
  7. excel绘制气泡图步骤
  8. android ios 微信 备份通讯录备份通讯录备份通讯录备份,微信通讯录备份在哪里?新版微信怎么备份通讯录?...
  9. 算法简介:不撞南墙不回头----深度优先搜索算法(DFS)
  10. 哈工大计算机学院崔启航,2014-2015年度哈尔滨工业大学学生先进集体及先进个人评选结果公示...