本文基于GO官网上的例子写就,主要帮助自己在学习go语言的过程中记忆,更多的是自己对其中一些概念的理解。
1.1 Go基础语法
函数
函数声明关键字func,包括函数名、参数列表(参数名 参数类型)、函数返回值,(需要指明返回类型,可以指定返回变量名)。具体格式如下:
1 | func funcName(a , b int) (sum int){ |
基本数据类型
int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、 uintptr 、byte(int8)、rune(int32)、float32、float64、complex64、complex128、 string、 bool
int,uint 和 uintptr 类型在32位的系统上一般是32位,而在64位系统上是64位。当你需要使用一个整数类型时,你应该首选 int,仅当有特别的理由才使用定长整数类型或者无符号整数类型。
变量
变量声明采用var关键字,如果声明时指定了初始值,则可以不写变量类型:
1 | var name string = "xxx" |
或短变量声明,在函数中, := 简洁赋值语句在明确类型的地方,可以用于替代 var 定义。函数外的每个语句都必须以关键字开始( var 、 func 、等等), := 结构不能使用在函数外。
1 | name := "xxx" |
var 语句定义了一个变量的列表;跟函数的参数列表一样,类型在后面。 var 语句可以定义在包或函数级别。
循环for
Go中只有一种for循环,基本的 for 循环包含三个由分号分开的组成部分:
- 初始化语句:在第一次循环执行前被执行
- 循环条件表达式:每轮迭代开始前被求值
- 后置语句:每轮迭代后被执行
且循环条件不能有圆括号,循环体需要大括号,其语法如下:
1 | sum :=0 |
如果前置条件、后置条件和分号都去掉,这就退化为C语言中的while循环了,语法如下:
1 | sum := 0; |
条件if
Go中的if语句同循环一样,不需要圆括号,具体执行语句需要大括号,其语法如下:
1 | if i < 10 { |
if 的便捷语句
跟 for 一样, if 语句可以在条件之前执行一个简单语句。
由这个语句定义的变量的作用域仅在 if 范围之内。包括在 if 的便捷语句定义的变量同样可以在任何对应的 else 块中使用。
包的定义
package是golang最基本的分发单位和工程管理中依赖关系的体现。每个golang源代码文件开头都拥有一个package声明,表示该golang代码所属的package。
要生成golang可执行程序,必须建立一个名为main的package,并且在该package中必须包含一个名为main()的函数。
在golang工程中,同一个路径下只能存在一个package,一个package可以拆成多个源文件组成
按照惯例,包名与导入路径的最后一个目录一致。
包的导入
包的导入使用import关键字
例如:
1 | import "fmt" |
导出名
在 Go 中,首字母大写的名称是被导出的。
在导入包之后,你只能访问包所导出的名字,任何未导出的名字是不能被包外的代码访问的。
Foo 和 FOO 都是被导出的名称。名称 foo 是不会被导出的。
零值
变量在定义时没有明确的初始化时会赋值为零值 。
零值是:
- 数值类型为 0 ,
- 布尔类型为 false ,
- 字符串为 “” (空字符串)。
类型转换
表达式 T(v) 将值 v 转换为类型 T 。
一些关于数值的转换例子:
1 | var i int = 42 |
与 C 不同的是 Go 的在不同类型之间的赋值时需要显式转换。
常量
常量的定义与变量类似,只不过使用 const 关键字。
常量可以是字符、字符串、布尔或数字类型的值。
常量不能使用 := 语法定义。
switch
你可能已经知道 switch 语句会长什么样了。
除非以 fallthrough 语句结束,否则分支会自动终止。
switch 的条件从上到下的执行,当匹配成功的时候停止。
没有条件的 switch
没有条件的 switch 同 switch true 一样。
这一构造使得可以用更清晰的形式来编写长的 if-then-else 链。
defer
defer 语句会延迟函数的执行直到上层函数返回。
延迟调用的参数会立刻生成,但是在上层函数返回前函数都不会被调用。
defer栈
延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。
指针
Go 具有指针。 指针保存了变量的内存地址。
类型 *T 是指向类型 T 的值的指针。其零值是 nil 。& 符号会生成一个指向其作用对象的指针。
1 | var p *int |
- 符号表示指针指向的底层的值。
1 | fmt.Println(*p) // 通过指针 p 读取 i |
这也就是通常所说的“间接引用”或“非直接引用”。
与 C 不同,Go 没有指针运算。
结构体
一个结构体( struct )就是一个字段的集合。
1 | package main |
结构体字段使用点号来访问。
结构体字段可以通过结构体指针来访问。
通过指针间接的访问是透明的。
结构体文法
结构体文法表示通过结构体字段的值作为列表来新分配一个结构体。
使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)
特殊的前缀 & 返回一个指向结构体的指针。
数组
类型 [n]T 是一个有 n 个类型为 T 的值的数组。
下面表达式定义变量 a 是一个有十个整数的数组。
1 | var a [10]int |
数组的长度是其类型的一部分,因此数组不能改变大小。 这看起来是一个制约,但是请不要担心; Go 提供了更加便利的方式来使用数组。
slice
一个 slice 会指向一个序列的值,并且包含了长度信息。
[]T 是一个元素类型为 T 的 slice。
len(s) 返回 slice s 的长度。
slice 可以包含任意的类型,包括另一个 slice。
对 slice 切片
slice 可以重新切片,创建一个新的 slice 值指向相同的数组。
如表达式:s[lo:hi],表示从 lo 到 hi-1 的 slice 元素,含前端,不包含后端。因此 s[lo:lo]是空的,而s[lo:lo+1]有一个元素。
构造 slice
slice 由函数 make 创建。这会分配一个全是零值的数组并且返回一个 slice 指向这个数组:
1 | a := make([]int, 5) // len(a)=5 |
为了指定容量,可传递第三个参数到 make:
1 | b := make([]int, 0, 5) // len(b)=0, cap(b)=5 |
slice 的零值是 nil
一个 nil 的 slice 的长度和容量是 0。
向 slice 添加元素
向 slice 的末尾添加元素是一种常见的操作,因此 Go 提供了一个内建函数 append 。 内建函数的文档对 append 有详细介绍。
1 | func append(s []T, vs ...T) []T |
append 的第一个参数 s 是一个元素类型为 T 的 slice ,其余类型为 T 的值将会附加到该 slice 的末尾。
append 的结果是一个包含原 slice 所有元素加上新添加的元素的 slice。
如果 s 的底层数组太小,而不能容纳所有值时,会分配一个更大的数组。 返回的 slice 会指向这个新分配的数组。
range
for 循环的 range 格式可以对 slice 或者 map 进行迭代循环。
当使用 for 循环遍历一个 slice 时,每次迭代 range 将返回两个值。 第一个是当前下标(序号),第二个是该下标所对应元素的一个拷贝。
可以通过赋值给 _ 来忽略序号和值。
如果只需要索引值,去掉 “ , value ” 的部分即可。
1 | package main |
map
map 映射键到值。
map 在使用之前必须用 make 来创建;
值为 nil 的 map 是空的,并且不能对其赋值。
map 的文法跟结构体文法相似,不过必须有键名。
若顶级类型只是一个类型名,你可以在文法的元素中省略它。
修改 map
在 map m 中插入或修改一个元素:
1 | var m = make(map[string]string) |
获得元素:
1 | name := m["name"] |
删除元素:
1 | delete(m, "name") |
通过双赋值检测某个键存在:
1 | name, ok = m["name"] |
如果 name 在 m 中, ok 为 true。否则, ok 为 false,并且 name 是 map 的元素类型的零值。
同样的,当从 map 中读取某个不存在的键时,结果是 map 的元素类型的零值。
函数值
函数也是值。他们可以像其他值一样传递,比如,函数值可以作为函数的参数或者返回值。
函数的闭包
Go 函数可以是一个闭包。闭包是一个函数值,它引用了函数体之外的变量。 这个函数可以对这个引用的变量进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。
例如,函数 adder 返回一个闭包。每个返回的闭包都被绑定到其各自的 sum 变量上。
练习:斐波纳契闭包
现在来通过函数做些有趣的事情。
实现一个 fibonacci 函数,返回一个函数(一个闭包)可以返回连续的斐波纳契数。
1 | package main |
方法
Go 没有类。然而,仍然可以在结构体类型上定义方法。
方法接收者 出现在 func 关键字和方法名之间的参数中。格式为:
1 | func (变量 类型) funcName(param1 type1,param2 type2)(return_param, return_type) { |
你可以对包中的 任意 类型定义任意方法,而不仅仅是针对结构体。
但是,不能对来自其他包的类型或基础类型定义方法。
接收者为指针的方法
方法可以与命名类型或命名类型的指针关联。
1 | package main |
刚刚看到的两个 Abs 方法。一个是在 *Vertex 指针类型上,而另一个在 MyFloat 值类型上。 有两个原因需要使用指针接收者。首先避免在每个方法调用中拷贝值(如果值类型是大的结构体的话会更有效率)。其次,方法可以修改接收者指向的值。
尝试修改 Abs 的定义,同时 Scale 方法使用 Vertex 代替 *Vertex 作为接收者。
当 v 是 Vertex 的时候 Scale 方法没有任何作用。Scale 修改 v。当 v 是一个值(非指针),方法看到的是 Vertex 的副本,并且无法修改原始值。
Abs 的工作方式是一样的。只不过,仅仅读取 v。所以读取的是原始值(通过指针)还是那个值的副本并没有关系。
接口
接口类型是由一组方法定义的集合。
接口类型的值可以存放实现这些方法的任何值
接口定义
1 | type Abser interface { |
接口实现
1 | type MyFloat float64 |
隐式接口
类型通过实现接口中的方法来实现接口。 没有显式声明的必要;所以也就没有关键字“implement“。
隐式接口解藕了实现接口的包和定义接口的包:互不依赖。
因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。
Stringers
一个普遍存在的接口是 fmt 包中定义的 Stringer。
1 | type Stringer interface { |
Stringer 是一个可以用字符串描述自己的类型。fmt
包 (还有许多其他包)使用这个来进行输出。
错误
Go 程序使用 error 值来表示错误状态。
与 fmt.Stringer 类似, error 类型是一个内建接口:
1 | type error interface { |
(与 fmt.Stringer 类似,fmt 包在输出时也会试图匹配 error。)
通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 nil, 来进行错误处理。
1 | i, err := strconv.Atoi("42") |
error 为 nil 时表示成功;非 nil 的 error 表示错误。
示例:
1 | package main |
Readers
io 包指定了 io.Reader 接口, 它表示从数据流结尾读取。
Go 标准库包含了这个接口的许多实现, 包括文件、网络连接、压缩、加密等等。
io.Reader 接口有一个 Read 方法:
1 | func (T) Read(b []byte) (n int, err error) |
Read 用数据填充指定的字节 slice,并且返回填充的字节数和错误信息。 在遇到数据流结尾时,返回 io.EOF 错误。
例子代码创建了一个 strings.Reader。 并且以每次 8 字节的速度读取它的输出。
1 | package main |