Go开发 之 基础语法(变量的声明、初始化、作用域,匿名变量,多变量赋值,变量逃逸分析)

文章目录

1、变量的声明

1.1、标准格式

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

var 变量名 变量类型

变量声明以关键字 var 开头,后置变量类型,行尾无须分号。
Go语言的变量类型有:

  • bool
  • string
  • int、int8、int16、int32、int64
  • uint、uint8、uint16、uint32、uint64、uintptr
  • byte // uint8 的别名
  • rune // int32 的别名 代表一个 Unicode 码
  • float32、float64
  • complex64、complex128

这里有更详细的类型说明:Go开发 之 基础知识(25个关键字、36个预定义标识符)https://shazhenyu.blog.csdn.net/article/details/103082047

所有的内存在 Go 中都是经过初始化的。

举例:

var a, b *int

1.2、简短格式

除 var 关键字外,还可使用更加简短的变量定义和初始化语法。

名字 := 表达式

需要注意的是,简短模式(short variable declaration)有以下限制:

  • 定义变量,同时显式初始化。
  • 不能提供数据类型。
  • 只能用在函数内部。

举例:

c,d := 1, "abc"

1.3、批量格式

为懒人提供的定义变量的方法:

var (
    a int 
    b []float32
    c func() bool
    d struct { 
        e int
    }
)

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

2、变量的初始化

2.1、标准格式

var 变量名 类型 = 表达式

每个变量会初始化其类型的默认值,例如:

  • 整型和浮点型变量的默认值为 0 和 0.0。
  • 字符串变量的默认值为空字符串。
  • 布尔型变量默认为 bool。
  • 切片、函数、指针变量的默认为 nil。

当然,依然可以在变量声明时赋予变量一个初始值。

2.2、编译器推导类型的格式

在标准格式的基础上,将 int 省略后,编译器会尝试根据等号右边的表达式推导 hp 变量的类型。

var hp = 100

等号右边的部分在编译原理里被称做右值。

2.3、声明并初始化

这是Go语言的推导声明写法,编译器会自动根据右值类型推断出左值的对应类型。

注意:由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。
若定义过,将会发生编译错误。

var 的变量声明还有一种更为精简的写法,例如:
纯文本复制

hp := 100

注意:如果 hp 已经被声明过,但依然使用:=时编译器会报错:

// 声明 hp 变量
var hp int
// 再次声明并赋值
hp := 10

注意:至少有一个新声明的变量在左值中,即便其他变量名可能是重复声明,编译器不会报错:

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

3、多个变量同时赋值

多重赋值时,变量的左值和右值按从左到右的顺序赋值。
例如:

var a int = 100
var b int = 200
b, a = a, b
fmt.Println(a, b)

多重赋值在Go语言的错误处理和函数返回值中会大量地使用。

例如,使用Go语言进行排序时就需要使用交换,代码如下:

type IntSlice []int
func (p IntSlice) Len() int           {  return len(p) }
func (p IntSlice) Less(i, j int) bool {  return p[i] < p[j] }
func (p IntSlice) Swap(i, j int)      {  p[i], p[j] = p[j], p[i] }

4、匿名变量(没有名字的变量)

在 Lua、R 等编程语言里,匿名变量也被叫做哑元变量。
匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。
匿名变量的特点是一个下画线 _ ,_ 本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。例如:

func GetData() (int, int) { 
    return 100, 200
}
func main(){ 
    a, _ := GetData()
    _, b := GetData()
    fmt.Println(a, b)
}

结果:

100 200

5、变量的作用域(即生命周期)

一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。

了解变量的作用域对我们学习Go语言来说是比较重要的,因为Go语言会在编译时检查每个变量是否使用过,一旦出现未使用的变量,就会报编译错误。如果不能理解变量的作用域,就有可能会带来一些不明所以的编译错误。

根据变量定义位置的不同,可以分为以下三个类型:

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。
变量的生命周期与变量的作用域有着不可分割的联系:

  • 全局变量:它的生命周期和整个程序的运行周期是一致的;
  • 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
  • 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。

5.1、局部变量

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。
局部变量不是一直存在的,它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁。

例如:

package main
import (
    "fmt"
)
func main() { 
    //声明局部变量 a 和 b 并赋值
    var a int = 3
    var b int = 4
    //声明局部变量 c 并计算 a 和 b 的和
    c := a + b
    fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}

5.2、全局变量

在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源文件需要使用“import”关键字引入全局变量所在的源文件之后才能使用这个全局变量。

全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。

Go语言程序中全局变量与局部变量名称可以相同,但是函数体内的局部变量会被优先考虑。例如:

package main
import "fmt"
//声明全局变量
var a float32 = 3.14
func main() { 
    //声明局部变量
    var a int = 3
    fmt.Printf("a = %d\n", a)
}

运行结果:

a = 3

5.3、形式参数

在定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。

形式参数会作为函数的局部变量来使用。例如:

func sum(a, b int) int { 
    fmt.Printf("sum() 函数中 a = %d\n", a)
    fmt.Printf("sum() 函数中 b = %d\n", b)
    num := a + b
    return num
}

6、变量逃逸分析

6.1、什么是栈

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

6.1.1、概念

栈只允许从线性表的同一端放入和取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序,如下图所示:
《Go开发 之 基础语法(变量的声明、初始化、作用域,匿名变量,多变量赋值,变量逃逸分析)》
往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。

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

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

6.1.2、变量和栈有什么关系

栈可用于内存分配,栈的分配和回收速度非常快。下面的代码展示了栈在内存分配上的作用:

func calc(a, b int) int { 
    var c int
    c = a * b
    var x int
    x = c * 10
    return x
}

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

6.2、什么是堆

堆在内存分配中类似于往一个房间里摆放各种家具,家具的尺寸有大有小,分配内存时,需要找一块足够装下家具的空间再摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往这个空间里摆放家具会发现虽然有足够的空间,但各个空间分布在不同的区域,没有一段连续的空间来摆放家具。此时,内存分配器就需要对这些空间进行调整优化,如下图所示:
《Go开发 之 基础语法(变量的声明、初始化、作用域,匿名变量,多变量赋值,变量逃逸分析)》
堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

6.3、堆和栈的区别

  • 堆(heap):堆是用于存放进程执行中被动态分配的内存段。它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态加入到堆上(堆被扩张)。当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);
  • 栈(stack):栈又称堆栈, 用来存放程序暂时创建的局部变量,也就是我们函数的大括号{ }中定义的局部变量。

举例:

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

上述代码中,函数 f 里的变量 x 必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。用Go语言的术语说,这个局部变量 x 从函数 f 中逃逸了。

相反,当函数 g 返回时,变量 *y 不再被使用,也就是说可以马上被回收的。因此,*y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由Go语言的 GC(垃圾回收机制)回收这个变量的内存空间。

在实际的开发中,并不需要刻意的实现变量的逃逸行为,因为逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

虽然Go语言能够帮助我们完成对内存的分配和释放,但是为了能够开发出高性能的应用我们任然需要了解变量的声明周期。例如,如果将局部变量赋值给全局变量,将会阻止 GC 对这个局部变量的回收,导致不必要的内存占用,从而影响程序的性能。

6.4、变量逃逸(Escape Analysis)——自动决定变量分配方式,提高运行效率

Go语言将这个过程整合到了编译器中,命名为“变量逃逸分析”。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。

6.4.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))
}

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

go run -gcflags "-m -l" 文件名.go

运行结果如下:
《Go开发 之 基础语法(变量的声明、初始化、作用域,匿名变量,多变量赋值,变量逃逸分析)》
程序运行结果分析如下:

  • 第 2 行告知“代码的第 19 行的变量 a 逃逸到堆”。
  • 第 3 行告知“dummy(0) 调用逃逸到堆”。由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在 main() 函数中继续存在。
  • 第 4 行,这句提示是默认的,可以忽略。

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

6.4.2、取地址发生逃逸

下面的例子使用结构体做数据,来了解结构体在堆上的分配情况,代码如下:

package main
import "fmt"
// 声明空结构体测试结构体逃逸情况
type Data struct { 
}
func dummy() *Data { 
    // 实例化c为Data类型
    var c Data
    //返回函数局部变量地址
    return &c
}
func main() { 
    fmt.Println(dummy())
}

执行逃逸分析:
《Go开发 之 基础语法(变量的声明、初始化、作用域,匿名变量,多变量赋值,变量逃逸分析)》
注意第 4 行出现了新的提示:将 c 移到堆中。这句话表示,Go 编译器已经确认如果将变量 c 分配在栈上是无法保证程序最终结果的,如果这样做,dummy() 函数的返回值将是一个不可预知的内存地址,这种情况一般是 C/C++ 语言中容易犯错的地方,引用了一个函数局部变量的地址。

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

6.4.3、原则

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

编译器觉得变量应该分配在堆和栈上的原则是:

  • 变量是否被取地址;
  • 变量是否发生逃逸。
    原文作者:沙振宇
    原文地址: https://blog.csdn.net/u014597198/article/details/103233461
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞