(二)golang变量

变量声明

标准格式

Go 语言的变量声明格式为:

var 变量名 变量类型

变量声明以关键字 var 开头,后置变量类型,行尾无须分号。

批量格式

批量定义变量的方法:

var (
    a int
    b string
    c []float32
    d func() bool
)

使用关键字var和括号,可以将一组变量定义放在一起。

变量初始化

变量初始化的标准格式

var 变量名 类型 = 表达式
var hp int = 100

编译器推导类型的格式

var hp = 100

短变量声明并初始化

hp := 100

注意: 在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错,代码如下:

conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn2, err := net.Dial("tcp", "127.0.0.1:8080")

上面的代码片段,编译器不会报err重复定义。
go语言可以多个变量同时赋值,使用多重赋值时,如果不需要在左值中接收变量,可以使用匿名变量(anonymous variable)_。

golang字符串

字符串转义符

package main
import (
    "fmt"
)
func main() {
    fmt.Println("str := \"c:\\Go\\bin\\go.exe\"")
}

定义多行字符串

在源码中,将字符串的值以双引号书写的方式是字符串的常见表达方式,被称为字符串字面量(string literal)。这种双引号字面量不能跨行。如果需要在源码中嵌入一个多行字符串时,就必须使用`字符,代码如下:

const str = ` 第一行
第二行
第三行
\r\n
`
fmt.Println(str)

代码运行结果:

第一行
第二行
第三行
\r\n

`叫反引号,两个反引号间的字符串将被原样赋值到 str 变量中。在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

Go语言字符类型(byte和rune)

字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。

Go 语言的字符有以下两种:
一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
另一种是 rune 类型,代表一个 UTF-8 字符。当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型实际是一个 int32。

使用 fmt.Printf 中的%T动词可以输出变量的实际类型,使用这个方法可以查看 byte 和 rune 的本来类型,代码如下:

var a byte = 'a'
fmt.Printf("%d %T\n", a, a)
var b rune = '你'
fmt.Printf("%d %T\n", b, b)

例子输出结果:

97 uint8
20320 int32

可以发现,byte 类型的 a 变量,实际类型是 uint8,其值为 'a',对应的 ASCII 编码为 97。

rune 类型的 b 变量的实际类型是 int32,对应的 Unicode 码就是 20320。

Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。

go语言指针

Go语言变量生命期
指针(pointer)概念在 Go 语言中被拆分为两个核心概念:
类型指针,允许对这个指针类型的数据进行修改。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。
切片,由指向起始元素的原始指针、元素数量和容量组成。

受益于这样的约束和拆分,Go 语言的指针类型变量拥有指针的高效访问,但又不会发生指针偏移,从而避免非法修改关键性数据问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,更为安全。切片发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go 语言中使用&作符放在变量前面对变量进行“取地址”操作。

格式如下:

ptr := &v    // v的类型为T

其中 v 代表被取地址的变量,被取地址的 v 使用 ptr 变量进行接收,ptr 的类型就为T,称做 T 的指针类型。代表指针。

指针实际用法,通过下面的例子了解:

package main
import (
    "fmt"
)
func main() {
    var cat int = 1
    var str string = "banana"
    fmt.Printf("%p %p", &cat, &str)
}

运行结果:

0xc042052088 0xc0420461b0

创建指针的另一种方法——new() 函数

Go 语言还提供了另外一种方法来创建指针变量,格式如下:
new(类型)

一般这样写:

str := new(string)
*str = "ninja"
fmt.Println(*str)

new() 函数可以创建一个对应类型的指针,创建过程会分配内存。被创建的指针指向的值为默认值。

Go语言变量生命期,Go语言变量逃逸分析

讨论变量生命期之前,先来了解下计算机组成里两个非常重要的概念:堆和栈。

栈###

栈(Stack)是一种拥有特殊规则的线性表数据结构。

  1. 概念
    栈只允许往线性表的一端放入数据,之后在这一端取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序。

往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。

从栈中取出元素时,只能从栈顶部取出。取出元素后,栈的数量会变少。最先放入的元素总是最后被取出,最后放入的元素总是最先被取出。不允许从栈底获取数据,也不允许对栈成员(除栈顶外的成员)进行任何查看和修改操作。

栈的原理类似于将书籍一本一本地堆起来。书按顺序一本一本从顶部放入,要取书时只能从顶部一本一本取出。

  1. 变量和栈有什么关系
    栈可用于内存分配,栈的分配和回收速度非常快。下面代码展示栈在内存分配上的作用,代码如下:
func calc(a, b int) int {
    var c int
    c = a * b
    var x int
    x = c * 10
    return x
}

代码说明如下:
第 1 行,传入 a、b 两个整型参数。
第 2 行,声明 c 整型变量,运行时,c 会分配一段内存用以存储 c 的数值。
第 3 行,将 a 和 b 相乘后赋予 c。
第 5 行,声明 x 整型变量,x 也会被分配一段内存。
第 6 行,让 c 乘以 10 后存储到 x 变量中。
第 8 行,返回 x 的值。

上面的代码在没有任何优化情况下,会进行 c 和 x 变量的分配过程。Go 语言默认情况下会将 c 和 x 分配在栈上,这两个变量在 calc() 函数退出时就不再使用,函数结束时,保存 c 和 x 的栈内存再出栈释放内存,整个分配内存的过程通过栈的分配和回收都会非常迅速。

堆在内存分配中类似于往一个房间里摆放各种家具,家具的尺寸有大有小。分配内存时,需要找一块足够装下家具的空间再摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往空间里摆放家具会存在虽然有足够的空间,但各空间分布在不同的区域,无法有一段连续的空间来摆放家具的问题。此时,内存分配器就需要对这些空间进行调整优化。
堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
变量逃逸(Escape Analysis)——自动决定变量分配方式,提高运行效率
堆和栈各有优缺点,该怎么在编程中处理这个问题呢?在 C/C++ 语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈;全局变量、结构体成员使用堆分配等。程序员不得不花费很多年的时间在不同的项目中学习、记忆这些概念并加以实践和使用。

Go 语言将这个过程整合到编译器中,命名为“变量逃逸分析”。这个技术由编译器分析代码的特征和代码生命期,决定应该如何堆还是栈进行内存分配,即使程序员使用 Go 语言完成了整个工程后也不会感受到这个过程。

  1. 逃逸分析
    使用下面的代码来展现 Go 语言如何通过命令行分析变量逃逸,代码如下:
package main
import "fmt"
// 本函数测试入口参数和返回值情况
func dummy(b int) int {
    // 声明一个c赋值进入参数并返回
    var c int
    c = b
    return c
}
// 空函数, 什么也不做
func void() {
}
func main() {
    // 声明a变量并打印
    var a int
    // 调用void()函数
    void()
    // 打印a变量的值和dummy()函数返回
    fmt.Println(a, dummy(0))
}

代码说明如下:
第 6 行,dummy() 函数拥有一个参数,返回一个整型值,测试函数参数和返回值分析情况。
第 9 行,声明 c 变量,这里演示函数临时变量通过函数返回值返回后的情况。
第 16 行,这是一个空函数,测试没有任何参数函数的分析情况。
第 23 行,在 main() 中声明 a 变量,测试 main() 中变量的分析情况。
第 26 行,调用 void() 函数,没有返回值,测试 void() 调用后的分析情况。
第 29 行,打印 a 和 dummy(0) 的返回值,测试函数返回值没有变量接收时的分析情况。

接着使用如下命令行运行上面的代码:

$ go run -gcflags "-m -l" main.go

使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化。

运行结果如下:

# command-line-arguments
./main.go:29:13: a escapes to heap
./main.go:29:22: dummy(0) escapes to heap
./main.go:29:13: main ... argument does not escape
0 0

程序运行结果分析如下:
输出第 2 行告知“main 的第 29 行的变量 a 逃逸到堆”。
第 3 行告知“dummy(0)调用逃逸到堆”。由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在其声明后继续在 main() 函数中存在。
第 4 行,这句提示是默认的,可以忽略。

上面例子中变量 c 是整型,其值通过 dummy() 的返回值“逃出”了 dummy() 函数。c 变量值被复制并作为 dummy() 函数返回值返回,即使 c 变量在 dummy() 函数中分配的内存被释放,也不会影响 main() 中使用 dummy() 返回的值。c 变量使用栈分配不会影响结果。

  1. 取地址发生逃逸
    下面的例子使用结构体做数据,了解在堆上分配的情况,代码如下:
package main
import "fmt"
// 声明空结构体测试结构体逃逸情况
type Data struct {
}
func dummy() *Data {
    // 实例化c为Data类型
    var c Data
    //返回函数局部变量地址
    return &c
}
func main() {
    fmt.Println(dummy())
}

代码说明如下:
第 6 行,声明一个空的结构体做结构体逃逸分析。
第 9 行,将 dummy() 函数的返回值修改为 *Data 指针类型。
第 12 行,将 c 变量声明为 Data 类型,此时 c 的结构体为值类型。
第 15 行,取函数局部变量 c 的地址并返回。Go 语言的特性允许这样做。
第 20 行,打印 dummy() 函数的返回值。

执行逃逸分析:

$ go run -gcflags "-m -l" main.go
# command-line-arguments
./main.go:15:9: &c escapes to heap
./main.go:12:6: moved to heap: c
./main.go:20:19: dummy() escapes to heap
./main.go:20:13: main ... argument does not escape
&{}

注意第 4 行出现了新的提示:将 c 移到堆中。这句话表示,Go 编译器已经确认如果将 c 变量分配在栈上是无法保证程序最终结果的。如果坚持这样做,dummy() 的返回值将是 Data 结构的一个不可预知的内存地址。这种情况一般是 C/C++ 语言中容易犯错的地方:引用了一个函数局部变量的地址。

Go 语言最终选择将 c 的 Data 结构分配在堆上。然后由垃圾回收器去回收 c 的内存。

  1. 原则
    在使用 Go 语言进行编程时,Go 语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆上的问题。编译器会自动帮助开发者完成这个纠结的选择。但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于 Go 语言,在 Java 等语言的编译器优化上也使用了类似的技术。

编译器觉得变量应该分配在堆和栈上的原则是:
变量是否被取地址。
变量是否发生逃逸。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,733评论 0 38
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 4,585评论 1 19
  • 指针是C语言中广泛使用的一种数据类型。 运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构; ...
    朱森阅读 3,423评论 3 44
  • 几种语言的特性 汇编程序:将汇编语言源程序翻译成目标程序编译程序:将高级语言源程序翻译成目标程序解释程序:将高级语...
    囊萤映雪的萤阅读 2,863评论 1 5