## 4.4.1 简介
声明变量的一般形式是使用 `var` 关键字:`var identifier type`。
需要注意的是,Go 和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。Go 为什么要选择这么做呢?
首先,它是为了避免像 C 语言中那样含糊不清的声明形式,例如:`int* a, b;`。在这个例子中,只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写(你可以在 [Go 语言的声明语法](http://blog.golang.org/2010/07/gos-declaration-syntax.html) 页面找到有关于这个话题的更多讨论)。
而在 Go 中,则可以和轻松地将它们都声明为指针类型:
~~~
var a, b *int
~~~
其次,这种语法能够按照从左至右的顺序阅读,使得代码更加容易理解。
示例:
~~~
var a int
var b bool
var str string
~~~
你也可以改写成这种形式:
~~~
var (
a int
b bool
str string
)
~~~
这种因式分解关键字的写法一般用于声明全局变量。
当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。
变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:`numShips` 和 `startDate`。
但如果你的全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写(第 4.2 节:可见性规则)。
一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。在第 5 章,我们将会学习到像 if 和 for 这些控制结构,而在这些结构中声明的变量的作用域只在相应的代码块内。一般情况下,局部变量的作用域可以通过代码块(用大括号括起来的部分)判断。
尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。
变量可以编译期间就被赋值,赋值给变量使用运算符等号 `=`,当然你也可以在运行时对变量进行赋值操作。
示例:
~~~
a = 15
b = false
~~~
一般情况下,只有类型相同的变量之间才可以相互赋值,例如:
~~~
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 (
a = 15
b = false
str = "Go says hello to the world!"
numShips = 50
city string
)
~~~
不过自动推断类型并不是任何时候都适用的,当你想要给变量的类型并不是自动推断出的某种类型时,你还是需要显式指定变量的类型,例如:
~~~
var n int64 = 2
~~~
然而,`var a` 这种语法是不正确的,因为编译器没有任何可以用于自动推断类型的依据。变量的类型也可以在运行时实现自动推断,例如:
~~~
var (
HOME = os.Getenv("HOME")
USER = os.Getenv("USER")
GOROOT = os.Getenv("GOROOT")
)
~~~
这种写法主要用于声明包级别的全局变量,当你在函数体内声明局部变量时,应使用简短声明语法 `:=`,例如:
~~~
a := 1
~~~
下面这个例子展示了如何在运行时获取所在的操作系统类型,它通过 `os` 包中的函数 `os.Getenv()` 来获取环境变量中的值,并保存到 string 类型的局部变量 path 中。
示例 4.5 [goos.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/goos.go)
~~~
package main
import (
"fmt"
"os"
)
func main() {
var goos string = os.Getenv("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` 的格式化输出的功能(第 4.4.3 节)。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.4.md#442-值类型和引用类型)4.4.2 值类型和引用类型
程序中所用到的内存在计算机中使用一堆箱子来表示(这也是人们在讲解它的时候的画法),这些箱子被称为 “ 字 ”。根据不同的处理器以及操作系统类型,所有的字都具有 32 位(4 字节)或 64 位(8 字节)的相同长度;所有的字都使用相关的内存地址来进行表示(以十六进制数表示)。
所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值:
[![](https://github.com/Unknwon/the-way-to-go_ZH_CN/raw/master/images/4.4.2_fig4.1.jpg?raw=true)](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/images/4.4.2_fig4.1.jpg?raw=true)
另外,像数组(第 7 章)和结构(第 10 章)这些复合类型也是值类型。
当使用等号 `=` 将一个变量的值赋值给另一个变量时,如:`j = i`,实际上是在内存中将 i 的值进行了拷贝:
[![](https://github.com/Unknwon/the-way-to-go_ZH_CN/raw/master/images/4.4.2_fig4.2.jpg?raw=true)](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/images/4.4.2_fig4.2.jpg?raw=true)
你可以通过 &i 来获取变量 i 的内存地址(第 4.9 节),例如:0xf840000040(每次的地址都可能不一样)。值类型的变量的值存储在栈中。
内存地址会根据机器的不同而有所不同,甚至相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同。
更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。
一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
[![](https://github.com/Unknwon/the-way-to-go_ZH_CN/raw/master/images/4.4.2_fig4.3.jpg?raw=true)](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/images/4.4.2_fig4.3.jpg?raw=true)
这个内存地址为称之为指针(你可以从上图中很清晰地看到,第 4.9 节将会详细说明),这个指针实际上也被存在另外的某一个字中。
同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。
当使用赋值语句 `r2 = r1` 时,只有引用(地址)被复制。
如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。
在 Go 语言中,指针(第 4.9 节)属于引用类型,其它的引用类型还包括 slices(第 7 章),maps(第 8 章)和 channel(第 13 章)。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.4.md#443-打印)4.4.3 打印
函数 `Printf` 可以在 fmt 包外部使用,这是因为它以大写字母 P 开头,该函数主要用于打印输出到控制台。通常使用的格式化字符串作为第一个参数:
~~~
func Printf(format string, list of variables to be printed)
~~~
在示例 4.5 中,格式化字符串为:`"The operating system is: %s\n"`。
这个格式化字符串可以含有一个或多个的格式化标识符,例如:`%..`,其中 `..` 可以被不同类型所对应的标识符替换,如`%s` 代表字符串标识符、`%v` 代表使用类型的默认输出格式的标识符。这些标识符所对应的值从格式化字符串后的第一个逗号开始按照相同顺序添加,如果参数超过 1 个则同样需要使用逗号分隔。使用这些占位符可以很好地控制格式化输出的文本。
函数 `fmt.Sprintf` 与 `Printf` 的作用是完全相同的,不过前者将格式化后的字符串以返回值的形式返回给调用者,因此你可以在程序中使用包含变量的字符串,具体例子可以参见示例 15.4 [simple_tcp_server.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_15/simple_tcp_server.go)。
函数 `fmt.Print` 和 `fmt.Println` 会自动使用格式化标识符 `%v` 对字符串进行格式化,两者都会在每个参数之间自动增加空格,而后者还会在字符串的最后加上一个换行符。例如:
~~~
fmt.Print("Hello:", 23)
~~~
将输出:`Hello: 23`。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.4.md#444-简短形式使用--赋值操作符)4.4.4 简短形式,使用 := 赋值操作符
我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,而这个时候再在 Example 4.4.1 的最后一个声明语句写上 `var` 关键字就显得有些多余了,因此我们可以将它们简写为 `a := 50` 或 `b := false`。
a 和 b 的类型(int 和 bool)将由编译器自动推断。
这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 `:=` 可以高效地创建一个新的变量,称之为初始化声明。
**注意事项**
如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:`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"
~~~
上面这行假设了变量 a,b 和 c 都已经被声明,否则的话应该这样使用:
~~~
a, b, c := 5, 7, "abc"
~~~
右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 `5`, b 的值是 `7`,c 的值是 `"abc"`。
这被称为 **并行** 或 **同时** 赋值。
如果你想要交换两个变量的值,则可以简单地使用 `a, b = b, a`。
(在 Go 语言中,这样省去了使用交换函数的必要)
空白标识符 `_` 也被用于抛弃值,如值 `5` 在:`_, b = 5, 7` 中被抛弃。
`_` 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。
并行赋值也被用于当一个函数返回多个返回值时,比如这里的 `val` 和错误 `err` 是通过调用 `Func1` 函数同时得到:`val, err = Func1(var1)`。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.4.md#445-init-函数)4.4.5 init 函数
变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
每一个源文件都可以包含且只包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。
示例 4.6 [init.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/init.go):
~~~
package trans
import "math"
var Pi float64
func init() {
Pi = 4 * math.Atan(1) // init() function computes Pi
}
~~~
在它的 init 函数中计算变量 Pi 的初始值。
示例 4.7 [user_init.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/user_init.go) 中导入了包 trans(在相同的路径中)并且使用到了变量 Pi:
~~~
package main
import (
"fmt"
"./trans"
)
var twoPi = 2 * trans.Pi
func main() {
fmt.Printf("2*Pi = %g\n", twoPi) // 2*Pi = 6.283185307179586
}
~~~
init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 `backend()`:
~~~
func init() {
// setup preparations
go backend()
}
~~~
**练习** 推断以下程序的输出,并解释你的答案,然后编译并执行它们。
练习 4.1 [local_scope.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/local_scope.go):
~~~
package main
var a = "G"
func main() {
n()
m()
n()
}
func n() { print(a) }
func m() {
a := "O"
print(a)
}
~~~
练习 4.2 [global_scope.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/global_scope.go):
~~~
package main
var a = "G"
func main() {
n()
m()
n()
}
func n() {
print(a)
}
func m() {
a = "O"
print(a)
}
~~~
练习 4.3 [function_calls_function.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/function_calls_function.go)
~~~
package main
var a string
func main() {
a = "G"
print(a)
f1()
}
func f1() {
a := "O"
print(a)
f2()
}
func f2() {
print(a)
}
~~~
- 前言
- 第一部分:学习 Go 语言
- 第1章:Go 语言的起源,发展与普及
- 1.1 起源与发展
- 1.2 语言的主要特性与发展的环境和影响因素
- 第2章:安装与运行环境
- 2.1 平台与架构
- 2.2 Go 环境变量
- 2.3 在 Linux 上安装 Go
- 2.4 在 Mac OS X 上安装 Go
- 2.5 在 Windows 上安装 Go
- 2.6 安装目录清单
- 2.7 Go 运行时(runtime)
- 2.8 Go 解释器
- 第3章:编辑器、集成开发环境与其它工具
- 3.1 Go 开发环境的基本要求
- 3.2 编辑器和集成开发环境
- 3.3 调试器
- 3.4 构建并运行 Go 程序
- 3.5 格式化代码
- 3.6 生成代码文档
- 3.7 其它工具
- 3.8 Go 性能说明
- 3.9 与其它语言进行交互
- 第二部分:语言的核心结构与技术
- 第4章:基本结构和基本数据类型
- 4.1 文件名、关键字与标识符
- 4.2 Go 程序的基本结构和要素
- 4.3 常量
- 4.4 变量
- 4.5 基本类型和运算符
- 4.6 字符串
- 4.7 strings 和 strconv 包
- 4.8 时间和日期
- 4.9 指针
- 第5章:控制结构
- 5.1 if-else 结构
- 5.2 测试多返回值函数的错误
- 5.3 switch 结构
- 5.4 for 结构
- 5.5 Break 与 continue
- 5.6 标签与 goto
- 第6章:函数(function)
- 6.1 介绍
- 6.2 函数参数与返回值
- 6.3 传递变长参数
- 6.4 defer 和追踪
- 6.5 内置函数
- 6.6 递归函数
- 6.7 将函数作为参数
- 6.8 闭包
- 6.9 应用闭包:将函数作为返回值
- 6.10 使用闭包调试
- 6.11 计算函数执行时间
- 6.12 通过内存缓存来提升性能
- 第7章:数组与切片
- 7.1 声明和初始化
- 7.2 切片
- 7.3 For-range 结构
- 7.4 切片重组(reslice)
- 7.5 切片的复制与追加
- 7.6 字符串、数组和切片的应用
- 第8章:Map
- 8.1 声明、初始化和 make
- 8.2 测试键值对是否存在及删除元素
- 8.3 for-range 的配套用法
- 8.4 map 类型的切片
- 8.5 map 的排序
- 8.6 将 map 的键值对调
- 第9章:包(package)
- 9.1 标准库概述
- 9.2 regexp 包
- 9.3 锁和 sync 包
- 9.4 精密计算和 big 包
- 9.5 自定义包和可见性
- 9.6 为自定义包使用 godoc
- 9.7 使用 go install 安装自定义包
- 9.8 自定义包的目录结构、go install 和 go test
- 9.9 通过 Git 打包和安装
- 9.10 Go 的外部包和项目
- 9.11 在 Go 程序中使用外部库
- 第10章:结构(struct)与方法(method)
- 10.1 结构体定义
- 10.2 使用工厂方法创建结构体实例
- 10.3 使用自定义包中的结构体
- 10.4 带标签的结构体
- 10.5 匿名字段和内嵌结构体
- 10.6 方法
- 10.8 垃圾回收和 SetFinalizer
- 第11章:接口(interface)与反射(reflection)
- 11.1 接口是什么
- 11.2 接口嵌套接口
- 11.3 类型断言:如何检测和转换接口变量的类型
- 11.4 类型判断:type-switch
- 11.5 测试一个值是否实现了某个接口
- 11.6 使用方法集与接口
- 11.7 第一个例子:使用 Sorter 接口排序
- 11.8 第二个例子:读和写
- 11.9 空接口
- 11.10 反射包
- 第三部分:Go 高级编程
- 第12章 读写数据
- 12.1 读取用户的输入
- 12.2 文件读写
- 12.3 文件拷贝
- 12.4 从命令行读取参数
- 12.5 用buffer读取文件
- 12.6 用切片读写文件
- 12.7 用 defer 关闭文件
- 12.8 使用接口的实际例子:fmt.Fprintf
- 12.9 Json 数据格式
- 12.10 XML 数据格式
- 12.11 用 Gob 传输数据
- 12.12 Go 中的密码学
- 第13章 错误处理与测试
- 13.1 错误处理
- 13.2 运行时异常和 panic
- 13.3 从 panic 中恢复(Recover)
- 13.4 自定义包中的错误处理和 panicking
- 13.5 一种用闭包处理错误的模式
- 13.6 启动外部命令和程序
- 13.7 Go 中的单元测试和基准测试
- 13.8 测试的具体例子
- 13.9 用(测试数据)表驱动测试
- 13.10 性能调试:分析并优化 Go 程序
- 第14章:协程(goroutine)与通道(channel)
- 14.1 并发、并行和协程
- 14.2 使用通道进行协程间通信
- 14.3 协程同步:关闭通道-对阻塞的通道进行测试
- 14.4 使用 select 切换协程
- 14.5 通道,超时和计时器(Ticker)
- 14.6 协程和恢复(recover)
- 第15章:网络、模版与网页应用
- 15.1 tcp服务器
- 15.2 一个简单的web服务器
- 15.3 访问并读取页面数据
- 15.4 写一个简单的网页应用
- 第四部分:实际应用
- 第16章:常见的陷阱与错误
- 16.1 误用短声明导致变量覆盖
- 16.2 误用字符串
- 16.3 发生错误时使用defer关闭一个文件
- 16.5 不需要将一个指向切片的指针传递给函数
- 16.6 使用指针指向接口类型
- 16.7 使用值类型时误用指针
- 16.8 误用协程和通道
- 16.9 闭包和协程的使用
- 16.10 糟糕的错误处理
- 第17章:模式
- 17.1 关于逗号ok模式
- 第18章:出于性能考虑的实用代码片段
- 18.1 字符串
- 18.2 数组和切片
- 18.3 映射
- 18.4 结构体
- 18.5 接口
- 18.6 函数
- 18.7 文件
- 18.8 协程(goroutine)与通道(channel)
- 18.9 网络和网页应用
- 18.10 其他
- 18.11 出于性能考虑的最佳实践和建议
- 附录