Go基础--变量

变量

一、变量

1.1.变量声明

变量起名规则

  1. 理论上变量不限制长度
  2. 使用驼峰
  3. 局部变量(作用域小)的,使用短变量(比如i),作用域越大的,变量名称越长且有意义

基本的声明

1
var name type = expression

可以省略type或者expression其中一个

  • 省略type,自动推断类型
  • 省略expression,使用该类型的默认值:如0、””、false;切片、指针、map、channel、函数,这些默认值是nil

通过var进行多个变量一起声明

1
2
3
4
5
6
7
8
9
10
11
var (
a = 15 // 自动推断类型
b = false
str = "Go says hello to the world!"
numShips = 50
city string
n int64 = 2 // 显示声明类型
HOME = os.Getenv("HOME")
USER = os.Getenv("USER")
GOROOT = os.Getenv("GOROOT")
)

短变量声明

1
name := expression
  • 短变量声明一般放在局部变量声明和初始化
  • var声明变量
    • 和初始化表达式类型不一致的局部变量
    • 后边才对变量赋值的情况,变量初始值不重要的情况

注意点

在同一块作用域中不能声明两次变量

1
2
var a int = 1
var a int = 2

短变量声明的注意点

1
2
3
4
5
a, c := 1, 2
a, c := 2, 3 // 编译错误,No new variables on the left side of ':='
---
a, c := 1, 2
b, c := 2, 3 // 支持这样赋值,这样相当于新声明一个b,但是没有重新声明c,而是给c赋值

1.2.值类型的变量

所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值

内存在计算机中用一堆箱子来表示,这些箱子被称为“字 (word)”

使用等号将一个变量的值赋给另一个的时候,实际上是在内存中的i值进行了拷贝

1
2
i := 7
j = i

1.3.引用类型的变量

可以通过&i 来获取变量 i 的内存地址,例如:0xf840000040(每次的地址都可能不一样)。值类型的变量的值存储在栈中

引用类型的变量(上图中的r1)存储的是r1的值所在的内存地址,或者内存地址中第一个字(word)所在的位置。

这个内存地址被称为指针,这个指针实际上也被存在另外的某一个字中。

同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。

当使用赋值语句 r2 = r1 时,只有引用(地址)的值被复制。

1.4.全局变量和局部变量

local_scope.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

var a = "G"

func main() {
n()
m()
n()
}

func n() { print(a) }

func m() {
a := "O"
print(a)
}

// GOG

global_scope.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

var a = "G"

func main() {
n()
m()
n()
}

func n() {
print(a)
}

func m() {
a = "O"
print(a)
}

// GOO

function_calls_function.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

var a string

func main() {
a = "G"
print(a)
f1()
}

func f1() {
a := "O"
print(a)
f2()
}

func f2() {
print(a)
}

// GOG

1.5.init函数中的变量

  • 变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
  • 每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。

Init函数的用途:

  1. 在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性

  2. 设定某些固定的自定义全局变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package trans

    import "math"

    var Pi float64

    func init() {
    Pi = 4 * math.Atan(1) // init() function computes Pi
    }
  3. 当一个程序开始之前调用后台执行的 goroutine

    1
    2
    3
    4
    func init() {
    // setup preparations
    go backend()
    }

1.6.变量的类型转换

go的类型转换必须要显示声明

1
2
3
4
valueOfTypeB = typeB(valueOfTypeA)
// example
a := 5.0
b := int(a)

具有相同底层类型的变量之间可以相互转换:

1
2
3
4
type IZ int
var a IZ = 5
c := int(a)
d := IZ(c)

二、指针

2.1.指针、变量、值、地址的关系

  • 指针的值,是一个变量的地址
  • 每个变量都有地址,但是【值不一定有地址。怎么理解?】
  • 使用一个指针引用一个值被称为间接引用(就是区别于直接引用变量)

2.2.指针的使用

  • 一个指针变量可以指向任何一个值的内存地址 它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节。

指针demo

1
2
3
4
5
6
7
8
9
package main
import "fmt"
func main() {
var i1 = 5
fmt.Printf("An integer: %d, its location in memory: %p\n", i1, &i1)
var intP *int
intP = &i1
fmt.Printf("The value at memory location %p is %d\n", intP, *intP)
}

改变指针指向的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
s := "good bye"
var p *string = &s
*p = "ciao"
fmt.Printf("Here is the pointer p: %p\n", p) // prints address
fmt.Printf("Here is the string *p: %s\n", *p) // prints string
fmt.Printf("Here is the string s: %s\n", s) // prints same string
}
/*
Here is the pointer p: 0x2540820
Here is the string *p: ciao
Here is the string s: ciao
*/

不能获取字面量或常量的地址

1
2
3
const i = 5
ptr := &i //error: cannot take the address of i
ptr2 := &10 //error: cannot take the address of 10
  • 指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝
  • 指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率
  • 被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。
  • 指针也可以指向另一个指针,并且可以进行任意深度的嵌套,导致你可以有多级的间接引用,但在大多数情况这会使你的代码结构不清晰。

对一个空指针的反向引用是不合法的

1
2
3
4
5
6
7
package main
func main() {
var p *int = nil
*p = 0
}
// in Windows: stops only with: <exit code="-1073741819" msg="process crashed"/>
// runtime error: invalid memory address or nil pointer dereference

三、new函数

new()方法创建一个未命名的T类型变量,初始化T类型零值,并返回其地址

1
x = new(T) // T表示类型

new只是语法上的便利,以下两者是等价的:

1
2
3
4
5
6
7
8
9
func newInt1() *int {
return new(int)
}

func newInt2() *int{
var tmp int
return &int
}

通过new创建的两个变量的地址是不一样的

1
2
3
a := new(int)
b := new(int)
println(a == b) // false

四、变量的生命周期

  • 一个变量的生命周期就是从声明不可访问
  • go是自动垃圾回收的,垃圾回收的依据就是:变量是否可达
  • 对于一些函数内/代码块里面的局部变量,执行完了,就会变得不可达(个人猜测)
  • 对于全局的变量,存放在堆里面,局部的,存放在栈里面(个人理解)

4.1.逃逸

1
2
3
4
5
6
7
var global *int

func f() *int{
a := 0
return &a
}

这样就算f执行完了,a是一个局部变量,但是还是可以通过global去找到a,这称为 a从f种逃逸。

五、作用域

  • 作用域是编译时属性,是声明在程序文本中出现的区域
  • 生命周期是运行时属性
  • 语法块,词法块

Refs