go语言基础-变量

菩提叶子 / 2023-08-04 / 原文

简介

声明变量的形式一般是使用var关键字:var identifier type

需要注意的是,Go 和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。Go 要为什么选择呢?

首先,它是为了避免像C语言中那样含糊不清的语句形式,例如:。int* a, b;在这个例子中,只有a指针而b不是。如果你想要这两个指针都是指针,则需要将它们分开书写(你可以在Go语言的声明语法页面找到关于这个话题的更多讨论)。

而在 Go 中,则可以很轻松地把它们都声明为指针类型:

var a, b *int

其次,这种语法能够按照从左到右的顺序阅读,使得代码更容易理解。

译文:

var a int
var b bool
var str string

你也可以改写成这样的形式:

var (
    a int
    b bool
    str string
)

这种因式划分关键字的编写方法一般用于声明全局变量。

当一个变量被声明后,系统自动赋予它该类型的零值:int0float32(64)0.0,bool为falsestring为空字符串,指针为nil。记住,Go中的所有内存都是经过初始化的。

标记的命名规则遵循骆驼命名法,即每个单词的首个单词小写,每个新单词的首字母大写,例如:numShipsstartDate

但如果你的全局变量希望能够被外部包所使用,则需要将第一个单词的首字母也大写(第4.2节:可见性规则)。

一个变量(常量、类型或函数)在程序中一定有作用范围,称作用域。如果一个变量在函数声明外部,则被认为是全局变量,可以整个包甚至外部包(在被导出)后)使用,无论你声明在哪个源文件里或在哪个源文件里调用该变量。

在函数内部声明的变量称为局部变量,它们的作用域只在函数内部,参数和返回值变量也是局部变量。在第5章,我们将学习到像和这些控制结构,而这些if结构for中中声明的变量的作用域仅在相应的代码块内。一般情况下,局部变量的作用域可以通过代码块(用大括号括起来的部分)来判断。

尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将暂时隐藏(结束内部代码块的执行隐藏)的外部同名变量又会出现,而内部同名变量则被释放),你的任何操作都只会影响内部代码块的局部变量。

变量可以在编译期间就被赋值,赋值给变量使用赋值等号=,当然你也可以在运行时对变量进行赋值操作。

a = 15
b = false

一般情况下,当变量a和变量b之间类型相同时,才能进行如a = b的赋值。

声明与赋值(初始化)语句也可以组合起来。

var identifier [type] = value
var a int = 15
var i = 5
var b bool = false
var str string = "Go says hello to the world!"

但是 Go 编译器的智能商已经高到可以根据变量的值来自动推断其类型,这有点像 Ruby 和 Python 这类动态语言,只不过它们是在运行时进行推断,而 Go 在编译时就已经完成了因此,你还可以使用下面的这些形式来声明及初始化变量:

var a = 15
var b = false
var str = "Go says hello to the world!"

自动推断类型并不在任何时候都适用,当您想要给变量的类型不是自动推断出的某种类型时,您还是需要显着的方式指定变量的类型,例如:

var n int64 = 2

然而,var a这种语法是不正确的,因为编译器没有任何可以用于自动推断类型的参考。变量的类型也可以在运行时实现自动推断,例如:

var (
    HOME = os.Getenv("HOME")
    USER = os.Getenv("USER")
    GOROOT = os.Getenv("GOROOT")
)

这种主要写法用于声明包级别的全局变量,当你在函数全局声明全局变量时,应使用简单的声明语法,:=例如:

a := 1

下面这个例子展示了如何通过runtime包在运行时获取所在的操作系统类型,以及如何通过os包中的函数os.Getenv()来获取环境变量中的值,并保存到string类型的局部变量path中。

package main

import (
    "fmt"
   "runtime"
    "os"
)

func main() {
    var goos string = runtime.GOOS
    fmt.Printf("The operating system is: %s\n", goos)
    path := os.Getenv("PATH")
    fmt.Printf("Path is %s\n", path)
}

如果您在 Windows 下运行可编程代码,则输出The operating system is: windows以及相应的环境变量的值;如果您在 Linux 下运行可编程代码,则输出The operating system is: linux以及相应的环境变量的值。

这里用到了Printf格式化输出的功能。

值类型和引用类型

程序中所占用的内存在计算机中使用一堆箱子来表示(这也是人们在讲解它的时候的画法),这些箱子被称为“字”。根据不同的处理器以及操作系统类型,所有的字都具有32位(4字节)或64位(8字节)的相同长度;所有的字都使用相关的内存地址来进行表示(以十六进制数表示)。

所有像intfloatboolstring这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值。

当使用等号=将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将i这个值进行了拷贝。

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

内存地址会根据机器的不同而选择不同的,甚至相同的程序在不同的机器上执行后也可能有不同的内存地址。因为每台机器可能有不同的内存布局,并且位置分配也可能不同。

更复杂的数据通常需要使用多个字符,这些数据一般使用引用类型保存。

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

这个内存地址被称为指针(你可以从上图很清晰地看到,第4.9节将详细说明),这个指针实际上也存在另外的某个字中。

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

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

如果r1的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2也受到影响。

在Go语言中,指针属于引用类型,其他的引用类型还包括slices,maps和channel。被引用的变量会存储在堆中中,便于进行垃圾回收,且比栈拥有更大的内存空间。

打印

函数Printf可以在fmt包外部使用,这是因为它以大写字母 P 开头,该函数主要用于打印输出到控制台。通常使用格式化字符串作为第一个参数:

func Printf(format string, list of variables to be printed)

这个格式化字符串可以包含一个或多个的格式化标识符,例如:%..,其中..可以被不同类型对应的标识符替换,如%s代表字符串标识符、%v代表使用类型的默认输出格式的标识符。这些标识符所对应的值从格式化字符串后的第一个逗号开始按照顺序相同添加,如果参数超过 1 个则同样需要使用分隔符分隔。使用这些占位符可以很好地控制输出的格式化文本。

函数fmt.SprintfPrintf的作用是两个的,不过首先将格式化后的字符串以返回值的形式返回给者,因此你可以在程序中使用包含变量的调用字符串。

函数fmt.Print并且fmt.Println会自动使用格式化标识符%v对字符串进行格式化,两者都会在每个参数之间自动增加空格,而晚上还会在字符串的最后加上一个换行符。例如:

fmt.Print("Hello:", 23)

将输出:Hello: 23

简单形式,使用:=赋值

我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,而这个时候上面第一个实例的最后一个语句语句中写上关键字就有些多余了,因此我们可以将它们简单写vara := 50b := false

ab的类型(intbool)将由编译器自动推断。

这是使用变量的默认形式,但是它只能在函数内部使用,而不能用于高效全局变量的声明与赋值。使用操作符可以创建一个新的变量,调用初始化声明:=

注意事项

如果在相同的代码块中,我们不能再次对于相同名称的变量使用初始化声明,例如:a := 20就是不被允许的,编译器会提示错误no new variables on left side of :=,但是a = 20是可以的,这是因为相同的变量赋予一个新的的值。

如果您在a使用之前定义了变量,则得到编译错误undefined: a

如果你声明了一个局部变量但没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子涉及的变量a

func main() {
   var a string = "abc"
   fmt.Println("hello, world")
}

尝试编译可能的代码将得到错误a declared and not used

另外,简单地给a定属性也不够的,这个值必须被使用,所以使用fmt.Println("hello, world", a)会删除错误。

但全局变量是允许声明但不使用。

其他的简短形式为:

相同类型的多个变量可以表述相同,如:

var a, b, c int

(这是将类型写在标识符后面的一个重要原因)

多个指标可以在同一行进行评价,如:

a, b, c = 5, 7, "abc"

上面这行代表了变量ab并且c都已经被声明了,否则的话应该这样使用:

a, b, c := 5, 7, "abc"

右边的这些值以相同的顺序赋予左边的变量,所以a的值是5b的值是7c的值是"abc"

这被称为家具同时分配。

如果您想要交换两个变量的值,则可以简单地使用a, b = b, a

(在Go语言中,这样省去了使用交换函数的必要)

空白标识符_也被用于废弃价值,如价值5在:_, b = 5, 7中被废弃。

_实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。

家具赋值也被用于当一个函数返回多个返回值时,比如这里的val和错误err是通过调用Func1函数同时得到:val, err = Func1(var1)

初始化函数

变量除了可以在全局声明中初始化,也可以在init()函数中初始化。这是一类非常特殊的函数,它不能够被人调用,而是在每个包完成初始化后自动执行,并且执行优先级比main()函数高。

每个源文件可以包含多个init()函数,同一个源文件中的init()函数会按照从上到下的顺序执行,如果一个包有多个源文件包含init()函数的话,则官方鼓励但不保证以文件名的顺序调用。初始化总是以单线程并且遵循包的依赖关系顺序执行。

一个可能的用途是在开始执行程序对数据进行检验或修复之前,以保证程序状态的正确性。

package trans

import "math"

var Pi float64

func init() {
   Pi = 4 * math.Atan(1) // init() function computes Pi
}

nit()函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子使用的backend()

func init() {
   // setup preparations
   go backend()
}