通向Golang的捷径【4. 基本结构和基本数据类型】

第 II 部分 核心结构与技巧

4.1 文件名, 关键字和标识符

Go 代码的文件扩展名为 go, 同时文件名应使用小写字母, 比如 scanner.go, 如果文件名中包含了多个部分, 可使用下划线进行分隔, 比如 scanner_test.go, 在文件名中不能包含空格和其他特殊字符. 在源码文件中会包含文本行, 但是文件长度并不存在限制.

在 Go 源码中, 名称的定义 (或称标识符) 与类 C 的所有语言一样, 即大小写敏感, 有效的标识符可从一个字母 (Unicode UTF-8 编码包含的任意字母), 或是下划线 _ 开头, 之后可包含 0, 字母或数字 (从属于 Unicode编码), 比如 X56,group1,_x23,i,Θε12, 以下是无效标识符,

1ab(从数字开始), case(不能使用关键字), a+b(不允许使用操作符)

下划线 _ 本身也是一个特殊字符, 又称空白标识符, 它可在声明或变量赋值时, 在其他标识符中使用 (也可在类型分配中使用), 但是与其对应的数值将被抛弃, 并在之后的代码中不会使用.

在某些情况下, 变量, 类型或函数并未包含一个名称, 这是因为, 在此刻的代码中这些元素并不真正需要, 而是为了提供所需的灵活性, 这些元素将称为匿名.

在 Go 代码中, 包含了 25 个关键字 (保留字):
在这里插入图片描述
这是为了在代码编译之前, 可简化代码的解析, 关键字当然不会用标识符.

除了关键字之外,Go 语言还提供了 36 个预定义标识符, 其中包含了数据类型和一些内建的基本函数, 如下:
在这里插入图片描述
一个 Go 程序中, 可包含关键字, 常量, 变量, 操作符, 类型和函数, 定界符可使用圆括号 (), 方括号 [], 花括号 ,标点符号可使用 . , ; : … ,Go 代码将使用语句创建, 一条语句中无须使用; 结尾 (这与类 C 语言不同),Go 编译器将自动在每条语句之后, 插入冒号 (作为结束符). 如果需要在一行文本中, 编写多条语句 ( 该写法并不推荐), 则必须使用; 进行分隔.

4.2 基本结构和语言组件

例 4.1

package main
import "fmt"
func main() {
	fmt.Println(”hello,world“)
)

4.2.1 包, 导入和可见性

包是一种代码结构, 一个程序可放入到包中 (package 通常会缩写为 pkg), 同时包也可使用其他的包.

每个 go 文件只能从属于一个包 (这类似于其他语言的库或名字空间), 大量不同的 go 文件可从属于一个包, 但这些文件名一般不与包名相同.从属于包的代码文件, 必须在代码文件的首行, 指定对应的包名, 比如 package main, 而 main 包将从属于 (等同于) 单个可执行文件, 每个 Go 应用都将包含一个 main 包.

一个 Go 应用可使用多个包, 即使只有一个 main 包, 也无须将所有代码都放入到一个文件中, 可在多个小文件的首行, 指定包名 package main, 如果需要编译其他包的源码文件, 比如 pack1, 那么目标文件将保存在pack1.a, 同时包名也将使用小写字母.

标准库

在 Go 语言的安装中, 已经装入了一些包, 它们被称为标准库, 在 Windows 系统中, 标准库将保存在pkg\windows_386 子目录, 在 Linux 系统中, 标准库将保存在pkg\linux_amd64 子目录 (32bit 子目录为 linux_amd32), 而标准库的完整路径为$GOROOT/pkg/KaTeX parse error: Expected group after '_' at position 5: GOOS_̲GOARCH/.

在 Go 标准库中包含了一些包 (比如 fmt,os), 当然也可创建用户包,

为了实现程序的构建, 包和包文件必须使用正确的次序进行编译, 这时将查看包的依赖性, 即是否需要编译其他包. 对于包来说, 其中包含的源文件将会一起被编译, 因此包编译可视为一个单元, 通常会基于包中每个子目录名的次序进行编译.

如果一个包进行了修改, 并重新编译时, 使用该包的应用程序也必须重新编译. 为了实现快速编译,Go 的包模型使用了显式依赖,Go 编译器可从目标文件 (.o) 中, 获取依赖类型的信息, 以确定目标文件的编译次序. 如果A.go 依赖于 B.go, 同时 B.go 又依赖于 C.go,
• 编译 C.go,B.go, 再编译 A.go
• 为了编译 A.go, 必须读取 B.o 而不是 C.o

在大型项目中, 上述编译机制, 可获得快速编译.

所有代码只需编译一次

在 Go 程序创建时, 需链接一组包 (这些包可使用 import 导入).

import ”fmt” 可告知 Go 语言, 应用程序需要使用 fmt 包 (函数, 组件或工具), fmt 包实现了规范化的 IO 操作, 包名必须封闭在” ” 引号中, 同时 import 还可导入包 (已编译) 的公共声明, 但是不会导入源码.

如果需要导入多个包, 可使用分隔语句:
在这里插入图片描述
还可使用更简洁和优雅的方法 (即提取关键字, 也可应用于 const,var,type),

import (
	"fmt"
	"os"
)

虽然还有更简短的方式 import (”fmt”; ”os”), 但 gofmt 工具会将这些编码风格强制转换成上述风格. 如果存在多个导入包, 以包名的字母顺序进行排序, 是一种更简明的方法.

如果包名未使用. 或/开头, 比如”fmt”,”container/list”,Go 将在整个 Go 安装树中, 搜索对应的导入包, 如果包名使用. 开头, 则从当前目录开始搜索, 如果包名使用/开头 (Windows 的用法相同), 将从一个绝对路径开始搜索.

除了 _ 符号之外, 在包中, 代码的标识符应当是唯一的, 不能存在名字冲突, 但是不同包中, 可使用相同的标识符, 因为包名将给出不同的限定值.

基于以下的可见性规则, 在包之外, 可导出包代码, 这也是编译器的强制要求.

可见性规则

当标识符 (常量, 变量, 类型定义, 函数和结构等) 开头是一个大写字母, 比如 Group1, 那么该标识符在包之外的代码 (导入包的应用程序) 中可见, 这被称为导出 (与 OO 语言的 public 相似), 如果标识符开头是一个小写字母, 它在包之外, 将不可见, 但在包中是可见的 (与 private 类似). 注意, 大写字母应当是 Unicode 码, 不允许是 ASCII 字符. 所以导入一个包, 可将包的公共对象, 导入代码中.

假定在 pack1 包中, 包含了 Thing 对象 (变量或函数), 当导入 pack1 包后, 可使用点标记, 访问 Thing, 如pack1.Thing.

同时包也视为名字空间, 有利于避免名字冲突, 在不同名称的两个包中, 可提供名称相同的变量, 比如 pack1.Thing和 pack2.Thing. 为了简化应用 ( 使用缩写, 避免名称冲突等功能), 可定义一个假名 (alias), 比如 import fm ”fmt”, 并在以下代码中, 使用假名 fm.

例 4.2 alias.go

在这里插入图片描述
值得注意, 如果导入包未被代码使用, 将会产生一个构建错误, 因为 Go 语言的设计理念是, 不需要的代码不应存在 (no unnecessary code).

包中的声明和初始化

在导入包语句之后, 包作用域的全局元素, 比如常量声明, 变量声明, 类型定义以及函数, 将导入到代码中, 并能够直接使用.

4.2.2 函数

函数声明的简单格式为 func functionName(), 圆括号之间的函数形参, 可为零个, 一个或多个 (需使用, 符号分隔), 它们将为函数提供输入变量, 同时在每个形参名称之后, 必须指定它的类型.

应当提供一个 main 函数 (通常是第一个函数), 否则将出现未定义的构建错误, undefined: main.main, main 不会有形参和返回类型 ( 这不同于 C 风格), 否则也将产生构建错误,func main must have no arguments and no return values results.

当应用程序开始执行, 完成初始化后, 首先调用的函数, 就是 main.main(), 它是应用程序的入口点 (这与 C 相同), 当 main.main 返回时, 应用程序将退出执行.

函数包含的代码 (即函数实体) 将封闭在花括号 ({ } |) 之间. 同时花括号 {应与函数名处于同一行, 否则编译器和 gofmt 将给出错误, syntax error: unexpected semicolon or newline before {, 而花括号} 应处于末行函数代码的下一行, 花括号的用法规则, 同样适合于 if,for 等控制语句.

而小型函数则可写在一行中, 比如:
在这里插入图片描述
一个典型的函数格式如下:
在这里插入图片描述
其中 parameter_list(形参列表) 的格式为 (param1 type1, param2 type2, …), return_value_list(返回值列表)的格式为 (ret1 type1, ret2 type2, …).

如果函数需在包外可见, 应当使用大写字母开头, 否则需使用小写字母开头. 语句行 fmt.Println(”hello, world”),调用了 fmt 包的函数 Println, 它可将字串形参打印在控制台中, 并给出一个换行符, 这与 fmt.Print(”hello, world\n”)功能一致. 函数 Print 和 Println 也可送入变量, 比如fmt.Println(arr), 可使用默认的输出格式, 打印出变量 arr,因此使用这两个函数, 可打印变量的数值, 所以也能用于调试功能.

当执行到花括号} 时, 函数将停止执行, 或是遇到 return 语句后, 程序将从函数调用的下一行开始执行.

程序正常退出时, 状态码为 0, 如果程序异常退出, 状态码将为整型, 比如 1, 因此可在脚本中, 测试一个应用程序的执行状态.

4.2.3 注释

例 4.3 hello_world2.go

在这里插入图片描述
上述程序可打印出国际化字符, Hello World; or こんにちは世界, 同时使用字符可编写注释, 注释的内容不会被编译, 但能被 godoc 工具所使用,

单行注释将使用//开头, 多行注释或块注释, 可使用/* 和 /, 同时/ 和 */不允许嵌套, 这有利于文档化和注释的提取.

每个包都会提供一个包注释 (放置在所有包代码之前), 并会使用块注释, 用于描述与包相关的信息, 以及包的所有功能, 一个包可分离成多个文件, 但是包注释只有一个, 当开发者在 godoc 工具中查找包的信息时,godoc将显示包注释, 这些注释文本或段, 可给出更多的信息, 同时注释文本的标点也应当书写正确.
在这里插入图片描述
程序顶层的类型定义, 常量, 变量和函数 (也就是所有可导出的名称), 都会包含一个注释, 这类注释将出现在对应元素之前, 同时函数的描述性注释, 也将出现在函数之前, 如下:
在这里插入图片描述
godoc 工具可收集程序中包含的注释, 并生成一个技术文档.

4.2.4 类型

变量 (以及常量) 可包含数据, 而数据则存在不同的数据类型 (简称类型), 使用 var 声明的变量, 会初始化为对应数据类型的零值, 一个数据类型会定义数值集合, 以及这些数值可进行的操作.

类型可以是基本类型 (或简单类型), 比如 int,float,bool,string, 也可以是结构化类型 (或复合类型), 比如 struct,array,slice,map, channel,interface, 以上给出了类型的行为.

在结构化类型中, 不能包含数值 nil(空), 因为 nil 也是结构化类型的默认值 (在 Objective-C 中存在 nil, 在Java 中存在 null, 在 C/C++ 中存在 NULL 或 0, 这些特定数值的意义是一致的), 同时不会出现类型的层次化.

函数也将给出一个返回类型, 即函数返回的变量类型, 该类型应写在函数名和形参列表之后, 如下:
在这里插入图片描述
在函数中, 可返回一个 typeFunc 类型的变量 var, 比如 return var, Go 语言的函数可返回多个变量, 返回类型可使用逗号分隔, 并封闭在圆括号中, 比如:
在这里插入图片描述
库函数 Atoi的原型为:
在这里插入图片描述
它可返回多个变量, 即 return var1, var2, 一个变量用于标记函数是否成功执行, 另一个变量将给出错误消息.

关键字 type 可定义用户类型, 如果需要自定义一个类型 (参见第 10 章), 也可为已存在类型, 定义一个假名,比如:
在这里插入图片描述
同时 int 被视为一种基本类型, 以此它的转换比较方便 (4.2.6 节), 如果需要自定义多个类型, 可使用关键字的提取格式, 如下:
在这里插入图片描述
完成编译后, 程序的所有数值都将对应一个类型, 因此编译器可为数值, 推断出一种类型. 另外 Go 是一种包含静态类型的语言.

4.2.5 Go 程序的一般结构

以下程序只是一个示例, 并无实际的功能, 但它给出了 Go 程序的一般结构, 程序给出的结构并不会绝对的, 编译器并不在意,main() 函数与变量声明的文本次序, 但是一致性的文本结构, 可提高 Go 代码的可读性, 所以在后续中, 都将采用以下的代码结构,
• 在导入声明之后, 将给出常量声明, 变量声明和类型定义
• 如果需要, 将提供 init() 函数, 它是每个包的特殊函数, 必须首先执行该函数
• 之后是 main() 函数 (只有 main 包, 才有 main 函数)
• 最后是剩余的函数, 首个类型的方法, 或是 main() 包含的其他函数, 以及按字母顺序排列的函数或方法.

例 4.4 gotemplate.go

在这里插入图片描述
以下是 Go 程序的执行次序:
• 按文本次序, 将 main 包指定的其他包导入
• 这些导入包还可递归导入其他包.
• 引入包的所有常量和变量, 之后是 init() 函数
• 完成 main 包的初始化, 将开始执行 main()

4.2.6 转换

有时一个数值必须强制转换成其他数据类型, 但 Go 语言无法实现隐含 (自动) 转换, 因此必须使用显式转换,而转换语法与函数调用很相似 (其中的数据类型可视为一个函数):
在这里插入图片描述
但这类转换只能在某些条件下成功, 比如小类型转换成大类型 ( int16 转换成 int32), 如果大类型需转换成小类型 ( 比如 int32 转换成 int16,float32 转换成 int), 将出现数值的部分丢失 (截断), 当转换无法实现, 且被编译器检测到, 将会产生一个编译期错误, 否则将会产生一个运行时错误.

基于同一基础类型的不同数据类型, 可进行转换:
在这里插入图片描述

4.2.7 命名方式

Go 语言的主要目标, 是提供一种简洁的可读性强的编程语言,gofmt 工具可对编码风格进行强制调整, 所以 Go的命名方式也应当短小, 简洁和有意义, 混用大写字母, 下划线的长命名方式, 常见于 Java 或 Python, 这类名称会影响可读性, 并且未给出包名, 而附加包名能更好地区分名称, 同时返回一个对象的方法或函数, 又提供了一个名词性名称, 而不是所需的动词性名称, 比如为了修改一个对象, 需使用 SetName 函数, 如果条件允许,Go将使用 MixedCaps 或 mixedCaps 组合, 来定义多词语名称, 而不是下划线.

4.3 常量

const 定义的常量, 其数值将不会发生变化, 这类数据只支持布尔类型, 数值类型 (整型, 浮点数或复数), 字串类型. 示例如下:
在这里插入图片描述
其中 [type] 为数据类型, 这是一个可选标识, 编译器可隐含推断与数值对应的类型.
在这里插入图片描述
无类型的常量数值, 可隐含获取到数据类型, 因为常量数值所处的环境, 会需要一个对应类型的数值, 这时无类型的常量数值, 将满足环境的要求.
在这里插入图片描述
在编译期内,必须对常量进行评估,而常量的定义可视为一个表达式的计算,只有当表达式的所有数值都有效时,表达式才可进行计算。
在这里插入图片描述
数值常量不存在大小限制以及符号位, 可给出任意精度, 同时也不会产生溢出.
在这里插入图片描述
分界符\可得到一个连续的字符串, 此时是常量的数值. 相比于不同类型的变量而言, 它不存在转换过程所产生的一些问题.

如果将常量分配给精度过低的变量, 也只有在这种情况下, 常量会产生溢出, 也将得到一个编译错误, 在常量分配中, 允许使用并列赋值:
在这里插入图片描述
常量还可实现枚举定义:
在这里插入图片描述
在上述定义中, 未知性别, 女性, 男性可使用 0,1,2 代替, 因此可使用对应数值进行判断, 比如在 switch/case 结构中,

在以下的枚举定义中,iota 可将作为枚举值:
在这里插入图片描述
首次使用 iota 时, 其值为 0, 而在之后的每一行中,iota 值都将 +1, 即 a=0,b=1,c=2, 上述定义可简写为:
在这里插入图片描述
iota 也可用于表达式, 比如 iota + 50, 在一个新的常量定义块 (枚举定义) 或是初始化声明中,iota 值又将返回0.

在应用程序的执行中, 常量值无法被修改, 如果向常量赋值, 将得到一个编译错误, 以下是 time 包定义的星期名:
在这里插入图片描述
以下将定义一个枚举类型:
在这里插入图片描述
注意, 常量标识符的命名应全部使用大写字符, 比如 const INCHTOWCM = 2.54, 这可增强可读性, 同时也不会与包的可见性规则相冲突.

4.4 变量

变量的声明通常会使用关键字 var, 比如 var identifier type, 在变量标识符之后, 写出数据类型, 这违反了大多数编程语言的习惯, 为什么 Go 语言的开发者会选择这种风格? 首先可去除掉 C 声明存在的一些模糊场景, 比如 int* a, b; 此时只有 a 是指针,b 不是指针, 为了声明两个指针, 必须重复使用星号, 对于相关主题的讨论, 可参考 http://blog.golang.org/2010/07/gos-declaration-syntax.html 页面.

在 Go 语言中, 可使用 var a, b *int 声明两个指针, 其次, 按照从左到右的阅读习惯, Go 语言的定义风格更容易理解, 比如:
在这里插入图片描述
也可写为:
在这里插入图片描述
上述格式主要用于全局变量的声明, 当变量声明时, 基于不同的数据类型, 会将 0 值或 null 值, 自动赋给变量,int 为 0,float 为 0.0,bool 为 false, string 为””(空串), 指针和结构为 nil, 即 Go 中所有内存都将进行初始化.

变量标识符的命名将使用骆驼拼写法 (camelCasing, 标识符开头为小写字母, 之后每个单词的首字母为大写),比如 numShips,startDate. 如果变量可被导出, 它的开头字母应为大写字母,

变量 (以及常量, 数据类型, 函数) 只能在程序的某一部分中有效, 这被称为作用域 (scope), 在函数之外声明的变量 (顶层变量), 具有全局作用域 (或包作用域), 它在包的所有源码文件中, 都是可见的.

在函数内声明的变量, 将具有本地作用域, 也就是说, 它只在函数内有效, 并可用于函数形参变量和返回变量,比如 if 和 for, 在控制结构中声明的变量, 只在控制结构内可见 (结构作用域), 大多数时候, 作用域可视为一个包含变量声明的代码块 (能使用 {} 进行封闭).

虽然标识符应当是唯一的, 在代码块中声明的标识符, 可在内部代码块中重新声明, 而内部变量具有优先级, 并可作为外部变量 (同名) 的影子, 这类使用应当加倍小心, 以免造成一些不易察觉的错误,

在编译期内, 变量可进行赋值 (使用赋值操作符 =), 在运行时, 变量数值可加入计算, 或是进行修改.
在这里插入图片描述
当变量 a 和 b 为相同类型时, 变量之间也可进行赋值, 如 a=b, 变量的声明和初始化, 也可组合在一起, 如下:
在这里插入图片描述
由于 Go 编译器足够智能, 可从变量数值推断出变量类型 (也被称为类型的自动推测, automatic type inference,这与 Python 和 Ruby 很相似, 但这类操作发生在运行时), 因此以下定义 (忽略数据类型) 也是正确的:
在这里插入图片描述
当然给出变量的类型信息, 可确定所需的变量类型, 因为推测的变量类型未必是正确的, 比如 var n int64 = 2

为变量初始化提供一个表达式, 并不是正确操作, 因为编译器无法进行表达式的计算, 这类计算只能在运行时进行, 比如:
在这里插入图片描述
在这里插入图片描述
使用 var 定义, 可声明全局变量或包变量, 但在函数中, 可使用简单的声明操作符:=, 实现变量的声明.

以下程序将描述操作系统的运行方式, 调用 os 包的 Getenv 函数 (用于获取环境变量) 可获得操作系统的环境变量, 之后会将操作系统的类型和环境路径, 送入本地字串变量.

例 4.5 goos.go

在这里插入图片描述
上述程序将打印出操作系统的类型:windows 或 linux 以及环境路径 path.

4.4.1 数值类型和引用类型

程序使用的 PC 机内存, 可视为一组数量庞大的内存单元(word), 所有内存单元都拥有相同的长度, 比如 32bit(4个字节) 或 64bit(8 个字节), 这与不同类型的 CPU 和操作系统有关, 同时所有内存单元都会使用一个内存地址 ( 采用 16 进制数) 进行标记.

所有的基础 (原始) 类型, 比如 int,float,bool,string 等, 都是数值类型, 可直接送入到内存, 如:
(int) i → 7(32bit 内存单元)

而复杂类型, 比如数值 (参见第 7 章) 和结构 (参见第 10 章), 也都是数值类型. 使用 = 符号, 可实现不同变量的数值分配, 如 j=i, 并能在内存中, 创建原始数值的一份副本 (copy).
(int) i → 7
(int) j → 7

使用 &i 符号, 可得到内存单元的地址, 比如 0xf840000040, 同时数值类型的变量都会包含在堆栈 (stack) 中. 另外, 不同 PC 机之间给出的内存地址并不相同, 甚至相同程序的不同执行中, 也会给出不同的内存地址, 这是因为分配给不同执行的物理内存并不相同.

更复杂的数据通常需要多个内存单元, 因此需将其处理成引用 (reference) 类型, 比如引用类型的变量 r1 保存了一个内存地址, 而这个内存地址中, 保存了 r1 值, 如下:
(引用变量) r1 → 地址 1 → r1 值
(引用变量) r2 → 地址 1 ↗

这类内存地址也被称为指针 (pointer), 如果引用类型可使用连续的内存地址 (这类内存布局策略为相邻策略), 并在计算过程中, 这类内存策略可得到最高效的存储性能, 因为内存地址只需加 1, 就可指向下一个存储单元. 进行 r2=r1 分配时, 只复制了引用地址. 当 r1 值发生变化时, 它的所有引用 (如 r1 和 r2) 都将发生变化.

在 Go 语言中, 指针与 slice,map,channel 一样, 都是引用类型, 而引用类型的变量都将保存在堆 (heap), 因为它比堆栈具有更大的内存空间, 所以也需要提供垃圾收集功能.

4.4.2 打印

Printf 函数在 fmt 包之外可见, 因为函数名的首个字母为大写, 同时该函数可实现控制台的文本输出, 但它的第一个形参, 需提供一个格式化字串, 原型如下:
在这里插入图片描述
格式化字串可使用多个格式化标记, 之后将使用数值, 替换这些格式化标记, 比如%s 可指定一个字串格式,%v指定一个默认格式, 如果存在多个替换数值, 需使用逗号进行分隔.

fmt.Sprintf 函数的功能与 Printf 相似, 但该函数只能返回一个格式化字串, 而 fmt.Print 和 fmt.Println 函数,可使用格式标记%v, 实现形参的自动格式化, 也就是在形参之间添加空格, 并能在行尾, 加入一个换行符, 比如:
在这里插入图片描述

4.4.3 赋值操作符:= 带来的简化

在变量声明中, 类型名和 var 都可省略, 比如 a := 50 或 b := false. 那么编译器将推测 a 和 b 的类型, 即 a 为int,b 为 bool, 这类声明方法应当是首选, 但是它只能用于函数内, 而不是在包中使用, 由于:= 可高效创建一个新变量, 因此它也被称为初始化声明. 注意: 在同一个代码块中, 重复使用 a := 20 则不允许, 编译器将生成一个错误消息,no new variables on left side of :=, 但 a = 20 可以工作, 这只是为 a 重新赋值.

如果使用一个未声明的变量, 编译器将产生一个错误消息:undefined: a, 如果声明的局部变量未被使用, 编译器也将产生一个错误消息, 如以下的 main() 函数:
在这里插入图片描述
如果修改为 Println(”hello, world”, a), 可消除编译器的错误, 另外, 未使用的全局变量可以接受.

变量操作的简写

同一类型的多变量声明:
在这里插入图片描述
多变量的顺序赋值:
在这里插入图片描述
首先 a,b,c 必须已经声明, 同时不允许出现 a, b, c := 5, 7, ”abc”. 而上述赋值也被称为并行赋值或并发赋值,

因此变量值的互换, 可使用 a, b = b, a(所以 Go 语言的 swap 函数被弃用), 使用下划线_可完成数值的弃用, 比如 _, b = 5, 7, 将弃用 5.同时 _ 是一个只写变量, 不需知道它的数值, 也是 Go 语言的一个声明变量, 有时可用于函数中多个返回值的选择.

当函数存在多个返回值时, 也可使用并发赋值, 如下:
在这里插入图片描述

4.4.4 Init 函数

在 Go 语言中, 全局声明与全局初始化被分离, 变量的初始化将包含在 init() 函数中, 这是一个无法被程序调用的特殊函数, 它可在包的 main() 函数之前, 或是在包导入的起始处, 被自动执行.

每个源码文件只能包含一个 init() 函数, 初始化只能使用单线程, 同时包的依赖关系也必须保证正确的次序.在开始程序执行之前, 可在该函数中进行必要的检查或验证.

例 4.6 init.go

在这里插入图片描述

例 4.7 use_init.go

在这里插入图片描述
导入 trans 包之后, 可直接使用 Pi 值.

在服务器应用中,init() 函数可能需要频繁使用, 比如 backend() 协程需要跳转到应用的起始处, 如下:
在这里插入图片描述

4.5 基本类型和操作符

在本节中, 将讨论布尔, 数值和字符数据类型.

在表达式中, 数值将与操作符一起被使用, 而这些数值需给出对应的类型, 而每一种类型都自己的操作符号集,用于处理该类型的数值, 如果使用了类型中未定义的操作符, 编译器将产生一个错误消息.

一元操作符只需一个数值, 但二元操作符则需要两个数值, 并且这两个数值必须是同一类型,Go 语言未给出类型的隐含转换, 如果需要类型转换, 只能使用显式转换 ( 参见 4.2 节), 所以 Go 是一种强类型的语言, 也不存在 C++ 和 Java 的操作符重载功能, 而表达式的计算, 将使用从左到右的默认次序.

同时操作符之间还是内建的优先级 (参见 4.5.3), 它可告诉我们, 在表达式中, 那些操作符具有最高的优先级,并首先被执行, 而使用圆括号 () 可改变优先级, 因为在圆括号中的表达式, 将首先被执行.

4.5.1 布尔类型 bool

在这里插入图片描述
该类型的数值将使用两个预定义常量 true 和 false, 这两个数值可用于关系操作符 == 和!= 的比较结果, 如下:
在这里插入图片描述
Go 语言对于数值的比较相当严格, 比较数值必须是相同类型, 如果它们都是接口 (参见第 11 章), 则必须实现了相同的接口类型, 如果两个比较数值中, 有一个为常量, 它必须兼容于另一个数值的类型, 如果以上条件无法满足, 一个数值则必须显式转换成另一个类型.

布尔常量和变量值可用于逻辑操作符 (not,and,or), 用于生成一个布尔值, 但逻辑语句并不是一条 Go 语言的完整语句.

在控制结构中, 将对布尔值进行检测, 这通常是二元操作符, 而不会是一元操作符, 如果 T 可描述一个为 true 的语句, 而 F 可描述一个为 false 的语句, 可得以下的逻辑操作:
在这里插入图片描述
在逻辑运算中, 也可使用 (), 调整运算次序, 在打印函数的格式化标识中, %t 可对应布尔值的打印格式, 同时布尔值常用于 if,for,switch 语句的条件检测,

布尔值和布尔函数中, 常用的一个命名规则是, 起始字串为 is 或 Is, 比如 isSorted, isFound, isFinished, isVisible, 这让 if 语句更像一条判断语句, 如 unicode.IsDigit(ch),

4.5.2 数值类型

4.5.2.1 int 和 float

数值类型中包含了整型, 浮点型以及复数的支持, 而这些类型的描述, 都支持二进制的补码表达 (参见http://en.wikipedia.org/wiki/Two’s_complement).

Go 语言中包含了与架构相关的类型, 如 int,uint,uintptr. 这些类型在不同的机器上, 将得到不同的数据位长度, 在 32bit 机器上,int 类型的长度为 32bit(4byte), 而 64bit 机器上,int 类型为 64bit(8byte), 对于 uint 类型来说, 结果一样, 而 uintptr 只是用于保存内存地址的指针变量, 同时 float 类型并未存在差异, 为使与架构相关的类型, 能够提供一个固定的尺寸, 则需要定义一组新类型, 如下:

• 整型:
int8 (-128 ∼ 127)
int16 (-32768 ∼ 32767)
int32 (−2,147,483,648 ∼ 2,147,483,647)
int64 (−9,223,372,036,854,775,808 ∼ 9,223,372,036,854,775,807)

• 无符号整型:
uint8 (可给出假名字节, 0 ∼ 255)
uint16 (0 ∼ 65,535)
uint32 (0 ∼ 4,294,967,295)
uint64 (0 ∼ 18,446,744,073,709,551,615)

• 浮点数 (float,IEEE754):
float32 (±10−45 ∼ ±3.4 ∗ 1038 )
float64 (±10−324 ∼ 1.7 ∗ 10308)

int 也是整型类型, 并能获得最快的处理速度, 整型的初始值 (默认值) 为 0, 浮点型的初始值为 0.0.

float32 类型的精度可保留 7 位十进制数,float64 类型的精度可保留 15 位十进制数, 由于不同精度的要求, 在进行浮点类型的 == 或!= 比较时, 应当十分小心, 比如小于精度限定值的数值确认. 有时会用到 float64 类型,因为在 math 包中, 所有函数会期待使用该类型.

数值可使用八进制描述 (将给出符号 0 前缀, 如 077), 或 16 进制描述 (将给出符号 0x 前缀, 如 0xFF), 以及包含 e 的科学计数法 (等级为 10 的次方), 比如 1e3 = 1000,6.022e23 = 6.022 * 1023 .

使用语句 a := uint64(0), 可实现 uint64 类型的显式转换, 由于 Go 是一种强类型语言, 不允许类型的混用, 而常量可视为无类型数值, 因此常量的混用可以接受.

例 4.8 type_mixing.go(无法编译)

在这里插入图片描述
编译器产生的错误消息为:
在这里插入图片描述
也就是 int16 类型无法赋值给 int32, 因为 Go 语言中不存在隐含转换. 在以下程序中, 使用一个显式转换, 来避免类型的不匹配:

例 4.9 casting.go

在这里插入图片描述

格式化字串

在打印函数的字串格式化中, 使用%d 可指定一个整型 (%x 或%X 可指定一个 16 进制描述),%g 可指定一个浮点类型 (%f 可指定一个浮点数, %e 可指定一个科学记数法),%0nd 可指定 n 个数字, 来表示一个整型, 数值开头为 0.%n.mg 表示十进制小数点之后, 可出现 m 个数字, 而小数点之前, 可出现 n 个数字, g 可更换为 e 或f, 比如, 以%5.2e 格式, 打印数值 3.4, 可得 3.40e+00.

数值的转换

如果出现转换 a32bitInt = int32(a32Float), 那么数值的小数部分将被截断, 通常情况下, 数值转换成更小的类型, 将产生信息的丢失, 为了防止精度的丢失, 应当转换成精度更高的一些类型, 或是自己编写一个转换函数,以执行安全的下行转换, 比如, 以下代码可将 int 转换为 uint8:
在这里插入图片描述
float64 类型可安全转换成 int:
在这里插入图片描述
如果 x 值不在整型范围内, 上述程序将终止, 并发出一个崩溃消息,

4.5.2.2 复数

在 Go 语言中, 还可使用以下类型:
complex64 (包含了 32bit 实部和虚部)
complex128 (包含了 64bit 实部和虚部)

一个复数的书写格式为 re+imj,re 为实部,im 为虚部,j 等于√−1, 如下:
在这里插入图片描述
如果 re 和 im 都为 float32 类型,complex64 变量 c 还可使用另一种函数描述:

c = complex(re, im) 而 real© 和 imag© 函数可提取复数的实部和虚部, 在打印函数的格式化字串中, %v 可指定一个复数格式, 或使用%f 格式, 列出复数的不同部分.

复数支持所有的算术运算, 在两复数的精度相同的情况下, 也只能使用 == 和!= 进行比较,cmath 包将给出复数操作的公共函数, 如果系统内存不紧张, 应使用 complex128 类型, 因为 cmath 包的所有函数都支持该类型.

4.5.2.3 位操作符

只能对整型变量进行位操作, 在打印函数中,%b 可指定一个二进制的格式化文本.

位与操作, 假定 T(true) 为 1,F(false) 为 0:
在这里插入图片描述
位或操作
在这里插入图片描述
位异或操作
在这里插入图片描述
−位清除, 使用&^ 符号可实现位清除 (等价于与非运算).

一元位补 (complement), 比如^2 = ^10 = -01 ^ 10 = -11

位移中包含了左移 (<<), 右移 (>>), 比如bitP << n 或 bitP >> n, 数值可向左或向右移动 n 位, 由于位移
而产生的空位, 将使用 0 充填, 因此左移一位, 可使数值增大一倍 (*2), 而右移一位, 可使数值缩小一倍 (/2).

描述内存资源的常量

使用<< 操作符和 iota, 可实现内存资源的类型定义:
在这里插入图片描述
还可用于通讯状态的标志位,
在这里插入图片描述

4.5.2.4 逻辑操作符

常用的逻辑操作符有==, !=, <, <=, >, >=, 同时逻辑操作符的运算还可完成赋值, 比如:
在这里插入图片描述

4.5.2.5 算术操作符

对于整型和浮点型来说, 常用的算术操作符为+,-,*,/.

取模操作符% 只能用于整型, 9 % 4 -> 1, 如果程序中, 出现除 0 操作, 将导致一个实时崩溃的产生, 第 13 章将给出该属性的测试, 而浮点数与 0.0 相除, 将得到一个无限值 +Inf.

在运算过程中, 可使用以下简写,+=, -=, *=, /=, %=. 还可使用加 1(++) 和减 1(–) 操作符, 但是 ++ 和–只
能作为语句使用, 不能在表达式中使用, 比如 n = i++ 是无效使用, 而 f(i++),a[i]=b[i++] 表达式也不允许,
因此 C,C++ 和 Java 的有效用法, 在 Go 中并不有效.

在操作过程中, 如果出现溢出, 将不会产生任何错误信息, 高位数值将被简单丢弃, 在这种情况下, 可选择常量,如果需要无边界尺寸的整型或有理数, 可选择标准库中 big 包的类型 big.Int 和 big.Rat,

4.5.2.6 随机数

比如游戏或统计的一些应用中, 需要使用随机数,rand 包可实现伪随机数的生成.

下例可打印出 10 个非负的随机整型值.

例 4.10 random.go

在这里插入图片描述
在这里插入图片描述
rand.Float32 和 rand.Float64 函数可返回一个伪随机数, 随机区间为 [0.0,1.0), 可知生成的随机数中, 不会包含 1.0,rand.Intn 函数需要一个整型参数 n, 并能返回一个非负的伪随机数, 随机区间为 [0,n).

也可使用 Seed 函数, 给伪随机数的生成, 提供一个起始值 (种子), 通常会选择当前的偏移时间 (时间片的累加值) 作为种子,

4.5.3 操作符和优先级

有些操作符具有最高优先级, 相同优先级的二元操作符, 会以从左到右的次序进行运算, 以下列出了所有操作符的优先级,7 为最高优先级,1 为最低优先级.
在这里插入图片描述
使用圆括号 () 可对优先级进行调整, 包含在圆括号中的表达式, 将首先进行运算.

4.5.4 类型假名

可为类型名指定另一个名称, 同时在代码中, 可使用新类型名, 来替换原有的类型名, 因此可使用类型名的缩写, 或是避免名称的冲突.

在 type TZ int 语句中, 为 int 类型指定了一个新名称 (假名)TZ, 它可能在代码中, 用于表示时区, 而使用 TZ也可声明一个 int 变量, 如下:

例 4.11 type.go

在这里插入图片描述
因此类型假名可创建一个易于理解的新类型名, 同时又可包含原有类型不具备的新方法, 参见第 10 章, 比如TZ 类型就可包含一个打印时区信息的方法.

4.5.5 字符类型

准确地说在 Go 语言中, 字符类型并不是一种类型, 它只是一种特殊的整型值, byte 类型是 uint8 的假名, 因为在 ASCII 编码中, 一个字符即为一个字节 (byte), var ch byte = ‘A’, 字符需使用单引号封闭.

在 ASCII 编码中,A 字符的十进制值为 65,16 进制值为 41, 因此以下代码等同于字符 A 的赋值:
在这里插入图片描述
同时 Go 语言也支持 Unicode 编码 (UTF-8), 一个 Unicode 字符在内存中, 可表示为一个 int 类型, 在文档中,通常写为 U+hhhh, 其中 h 为一个 16 进制数字, 在 Go 语言中给出了一个 rune 类型, 它是 int32 的假名. 而在代码中书写一个 Unicode 字符, 应在 16 进制值之前, 添加\u 或U.

一般情况下,int16 或 int 类型包含了 2 个字节, 而\U 字符需要 4 个字节, 所以\u 需跟随 4 个字节, 而\U 需跟随 8 个字节, 如下:
在这里插入图片描述
格式化标记%c 可输出字符,%v 和%d 可输出字符的整型值,%U 可输出 U+hhhh 格式值.

unicode 包中提供了以下字符测试的函数, 假定 ch 是一个字符:
• 字符是否是字母: unicode.IsLetter(ch)
• 字符是否是数字: unicode.IsDigit(ch)
• 字符是否是空格: unicode.IsSpace(ch)

上述函数将返回一个布尔值,utf8 包给出的函数需使用 rune 类型.

4.6 字符串

字符串是 UTF-8 字符的一个序列 (如果是 ASCII 编码, 可能是一个字节, 如果是 UTF-8 编码, 也可能是
2-4 字节),UTF-8 编码的使用更加广泛, 它是文本文件,XML 文件和 JSON 字串的标准编码, 但是该编码描述一个字符, 需要 4 个字节, 而 ASCII 编码只需一个字节,Go 语言的字符串是一个可变宽度的字符序列, 而C++,Java,Python 语言则使用定长字符串 (Java 字符串通常使用 2 个字节), 而 Go 语言的字符串和文本文件有利于降低对内存和磁盘空间的占用, 由于 UTF8 是标准编码, 因此 Go 语言无须对字符串进行编码和解码,这是其他语言需要完成的字串处理.

由于字符串是一种不可变的数值类型, 所以字符串一旦创建, 将无法修改字符串的内容, 或是使用字符串的另一种组织方式, 即一个定长数组, 以下给出了字符串的两种构建方法:
• 可解析的字符串: 使用双引号 (” ”) 封闭, 还可使用以下的转义字符:
\n 表示一个换行符
\r 表示一个回车
\t 表示点击一次 tab 键
\u 或\U 表示一个 Unicode 字符
转义字符\还可对功能字符 (” ” ‘’ ) 进行转义.

• 流式字符串: 使用单撇号 (‘ ‘) 封闭, 这类字符串无法被解析, 但可跨越多行, 例如‘This is a raw string \n‘中存在的换行符\n 无法被解析.

字符串会有一个长度, 但是不会与 C/C++ 一样, 使用特殊字符来结束字符串, 字符串的初始值为空串 (” ”),在字符串之间, 使用比较操作符 (== != < <= >= >) 时, 将在内存中, 对两个字符串的字节内容逐个比较,
len() 函数可获取字符串变量的长度, 比如 len(str).

使用标准索引方式 ([ ], 索引值从 0 开始), 可对流式字符串进行访问, 如下:
• 获取字符串变量 str 的第一个字节: str[0]
• 获取 str 的第 i 个字节: str[i]
• 获取 str 的最后一个字节: str[len(str)-1]

只使用 ASCII 编码, 字符才能进行转换, 可以获取字符串中字符的地址, 比如 &str[i] 是合法的.

字符串的合并

两个字符串 s1 和 s2 能合并成一个字符串 s, 即 s := s1 + s2, 这时 s2 将附加到 s1 之后, 并得到一个新的字符串 s, 以下语句将得到一个多行字符串:
在这里插入图片描述
应当注意,+ 需放置在第一行, 同时编译器会将; 插入第一行, 在字符串处理中, 可使用简写符号 +=:
在这里插入图片描述
在循环中实现字符串合并, 如果使用 + 号并不高效, 应当选择 strings.Join() 函数 ( 参见 4.7.10 节), 因为它可写入一个字节缓冲器

在第 7 章, 会将字符串视为一个 byte(int) 类型的切片 (slice), 因此切片索引可在字符串中使用, 在 5.4.1 节的for 循环中, 索引值只能返回字节流, 如果字符串使用了 Unicode 字符, 可必须使用 for-range 循环, 在下一节中, 将学习到字符串处理的一些方法, 比如 fmt 包的 fmt.Sprint(x) 函数, 它可生成一个格式化字符串,

4.7 strings 包和 strconv 包

字符串是一种基本的数据结构, 每一类语言都会提供一些预定义函数, 来完成字符串的处理, 因此 Go 语言也提供了 strings 包, 以下将给出包中的一些函数.

4.7.1 前缀和后缀

HasPrefix 可测试字符串 s 的开头, 是否与另一个字符串相同:
在这里插入图片描述
HasSuffix 可测试字符串 s 的结尾, 是否与另一个字符串相同:
在这里插入图片描述

例 4.13 presuffix.go

在这里插入图片描述注意, 上例中使用了转义字符.

4.7.2 字符串的包含

检测字符串 s 中是否包含了子串 substr:
在这里插入图片描述

4.7.3 字符串的匹配位置

在字符串 s 中, 返回 str 子串出现的第一个位置, 如果未包含 str 子串, 则返回-1.
在这里插入图片描述
在字符串 s 中, 返回 str 子串出现的最后一个位置, 如果未包含 str 子串, 则返回-1.
在这里插入图片描述
如果 ch 不是 ASCII 字符, 可使用 strings.IndexRune(s string, ch int) int 函数.

例 4.14 index_in_string.go

在这里插入图片描述

4.7.4 子串替换

在字符串 s 中, 将前 n 个 old 子串都替换成 new 子串, 并返回 str 的一个副本, 如果 n=-1, 则表示所有old子串都将替换成 new 子串.
在这里插入图片描述

4.7.5 子串计数

在字符串 s 中, 子串 str 出现的次数.
在这里插入图片描述

例 4.15 index_in_string.go

在这里插入图片描述

4.7.6 字符串的重复复制

将字符串 s 的 count 个副本, 合并成一个新字符串.
在这里插入图片描述

例 4.16 repeat_string.go

在这里插入图片描述

4.7.7 字符串的修改

ToLower 可将字符串中所有 Unicode 大写字母, 改为小写字母.
在这里插入图片描述
ToUpper 可将字符串中所有 Unicode 小写字母, 改为大写字母.
在这里插入图片描述

例 4.17 toupper_lower.go

在这里插入图片描述
在这里插入图片描述

4.7.8 字符串的整理

strings.TrimSpace(s) 可将字符串中存在的空格全部移除, 如果需要从字符串中, 移除一个字符串 cut, strings.Trim(s,”cut”). 例如 strings.Trim(s,”\r\n”) 可从字符串 s 中, 移除\r 和\n 字符, 但上述两个字符之间, 会包含其他一些字符, 通常是字符串 s 从左侧到右侧的所有字符, 因此 strings.Trim(s,”\r\n”) 将删除字符串 s 的所有字符,如果只需删除字符串的某些字符, 可使用 TrimLeft 或 TrimRight.

4.7.9 字符串的分段

如果分段特征字符为空格,strings.Fields(s) 可将字符串 s 基于一个或多个连续空格, 分割成多个字符串, 并返回字符串 s 的一个子串切片 [ ]string, 或是一个空串.

如果分段特征字符为特殊字符,strings.Split(s, sep) 的功能与 Fields 相同, 只不过分段的字符可以自定义.

由于返回值为 [ ]string, 因此该函数常用于 for-range 循环,

4.7.10 切片合并

将所有切片以特定字符分隔, 合并成一个新字串, 以下示例将给出简单用法:
在这里插入图片描述

例 4.18 strings_splitjoin.go

在这里插入图片描述
在这里插入图片描述

4.7.11 字符串读取

上述包还提供了一个函数 strings.NewReader(str), 它可生成一个指向读取器的指针, 并方便以下函数对字符串进行处理:
• Read() 可返回一个 [ ]byte.
• ReadByte() 和 ReadRune() 可从字符串中, 读取下一个字节或 rune.

4.7.12 字符串转换

strconv 包提供了字符串的转换函数, 并且包含了一些变量, 用于计算当前平台下 int 类型的位数, 如 str-
conv.IntSize, 将某个 T 类型变量转换成字符串类型, 通常都会成功.

将数字转换成字符串, 可使用以下函数:
• strconv.Itoa(i int) string: 可返回十进制数值的字符串.
• strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int) string: 可将 64bit 浮点数转换成字符串, 并对应不同的 fmt 格式 (b,e,f,g), 不同的精度 prec, 以及不同的位数 bitSize(32,64).

如果将字符串转换成某个 tp 类型, 则未必成功, 有可能会抛出一个运行时错误:
在这里插入图片描述
为将字符串转换成数字, 可使用以下函数:
• strconv.Atoi(s string) (i int, err error): 将字符串转换成 int 类型.
• strconv.ParseFloat(s string, bitSize int) (f float64, err error): 将字符串转换成 64bit 浮点数.

上述函数可返回两个值, 一是转换数值, 一是错误代码, 因此在函数调用时, 可实现多重赋值,
在这里插入图片描述
在以下程序中, 我们可忽略空白标识符 (_) 所导致的转换错误, 即 an, _ = strconv.Atoi(origStr)

例 4.19 string_conversion.go

在这里插入图片描述
在这里插入图片描述
5.1 节将在 if 语句的讨论中, 对一个可能的错误进行检查, 并返回检查结果.

4.8 时间和日期

time 包提供了一个数据类型 time.Time, 它可用于时间和日期的测量和显示.

time.Now() 可获取当前时间, 同时使用 t.Day(),t.Minute() 等函数, 可获取当前时间的一部分 (年, 月, 日, 小时, 分钟, 秒等), 如下:
在这里插入图片描述
Duration 类型可描述两时刻之间的消耗时间, 它是一个纳秒计数值, 并且是一个 int64 类型,Location 类型可描述某一时区的时间,UTC 可描述通用坐标时间 ( Universal Coordinated Time).

另外还包含了一个预定义函数,func (t Time) Format(layout string) string, 它可将时间值 t 格式化为一个字串, 并且还提供了一些预定义格式, 比如 time.ANSIC,time.RFC822. 而这些格式都是标准时间的显示格式, 比如:
在这里插入图片描述

例 4.20 time.go

在这里插入图片描述
在这里插入图片描述
有时应用程序需要在一定时间之后, 或是周期时间内 (事件处理), 进行一些处理, 这时需要使用time.After 和time.Ticker, 参见 14.5 节, 而 time.Sleep(Duration d) 函数可将当前处理暂停一段时间 d, 参见 14.1 节.

4.9 指针

与 Java 和.NET 不同,Go 语言为编程者开放了部分数据结构的指针, 其余数据结构则无指针, 同时在应用程序中, 无法进行指针的运算, 而编程者只能对基本的内存布局进行控制, 因此在 Go 语言中, 用户只能进行了一些必要的控制, 比如控制数据结构的整体尺寸, 内存分配的次数, 内存访问的模式, 这些都是影响性能的重要部分, 在进行系统编程, 与操作系统和网络密切相关的部分中, 指针都是决定性能的主要因素.

由于指针概念在一些主流的 OO 语言中被隐藏, 因此需要从头开始介绍一下指针概念, 以便在后续章节中, 快速掌握指针的应用.

应用程序会在内存中保存数值, 而每个内存单元都有一个地址, 该地址值通常采用 16 进制数表示, 比如 0x6b0820或 0xf84001d7f0, 在 Go 语言中, 也提供了一个取地址操作符 (&), 它可得到保存变量的内存单元的地址.
在这里插入图片描述
注意, 上述代码的输出结果在每次执行中, 都会不同.

这些内存地址也可保存在一种特殊的数据类型中, 即指针, 比如一个指向 int 类型的指针, 可标记为 *int, 同时我们可为该类型声明一个指针变量:
在这里插入图片描述
在打印的格式化文本标记中, 使用%p 可指定指针变量的输出, intP 保存了 i1 的内存地址, 因此 intP 指向了i1 的存储单元, 即 intP 引用了变量 i1.

在 32bit 系统中, 地址值为 4 个字节, 在 64bit 系统中, 地址值为 8 个字节, 当然地址值的大小与地址中保存的数值无关, 因此指针声明时, 可使用任意类型, 同时需在数值类型之前, 使用 * 符号 (前缀), 而 * 被称为类型修饰符, 指针指向数值又被称为间接引用.

新声明的指针不允许分配一个数值为 nil 的变量, 指针变量通常缩写为 ptr, 在表达式 var p *type 中, 指针名和 * 之间给出了一个空格, 但是 var p*type 书写方式也正确, 如果在更复杂的表达式中, 这类书写方式有可能被误解为乘法!!

* 符号也可放置在指针名之前, 比如 *intP, 这等同于获取内存地址的保存数值, 这被称为反向引用,
在这里插入图片描述

例 4.21 pointer.go

在这里插入图片描述
在这里插入图片描述
在以下代码中, 将为 *p 分配一个新值, 以修改引用变量的数值 (它是一个字符串).

例 4.22 string_pointer.go

在这里插入图片描述
通过指针 *p, 可修改引用变量的数值.
在这里插入图片描述
但是文本或常量的内存地址无法获取, 如下代码:
在这里插入图片描述
Go 语言与其他底层 (系统) 语言 (比如 C,C++,D) 很相似, 包含了指针的概念, 但指针的运算 (或称指针的
算术, 比如 ptr+2, 即跳转到当前地址 +2 的内存地址) 经常会引发 C 语言的内存访问错误, 并造成程序的崩溃, 因此在 Go 语言中, 不允许指针的运算, 所以 Go 是一种内存安全型的语言, 同时 Go 语言的指针, 更像是Java,C#,VB.NET 语言的引用 (reference).
在这里插入图片描述
指针的另一个优势是, 可实现变量引用的传递 (比如函数形参的传递), 而不需要传递变量的副本, 指针传递的成本更低, 只有 4 个或 8 个字节, 应用程序所需的变量个数, 即意味着所占用的内存量, 因此使用指针可减少内存的占用并提高效率. 在内存中存储的变量, 至少有一个指针指向它, 同时它们的生命期也是相互独立的. 如果单纯地禁用指针, 将导致性能的下降.

同时指针还可指向其他的指针, 并且能够实现任意深度的嵌套, 所以可得到多重等级的间接引用, 但在大多数情况下, 这类功能无法保证代码的清晰度. 因此 Go 语言为了保证大多数情况的简单使用, 对编程者隐藏了间接引用, 而是提供了自动化的反向引用.

对 nil 指针执行反向引用, 如以下示例的第 2 行, 这将是非法的, 并且会造成程序的崩溃.

例 4.22 testcrash.go

在这里插入图片描述

在这里插入图片描述

发布了80 篇原创文章 · 获赞 10 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/osoon/article/details/103747027
今日推荐