Golang 基础语法学习笔记
常量
关键词:
const
用法:
- 单行声明:
const variableName [Type] = value
- 并行声明:
const p1, p2, p3 = v1, v2, v3
- 多行声明:
1
2
3
4
5
6
7
8
9
10
11const beef, two, c = "eat", 2, "veg"
const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6
const (
Monday, Tuesday, Wednesday = 1, 2, 3
Thursday, Friday, Saturday = 4, 5, 6
)
const (
a = iota
b = iota
c = iota
)- 单行声明:
变量
声明格式
单行变量声明格式var name [type] [= val]
多行变量声明格式:
1 | var ( |
也可以同时给多个值声明类型:var a, b, c int
初始化
若未显示初始赋值,则:
- int: 0
- float: 0.0
- string: 空字符串“”
- bool: false
- ptr: nil
且声明时变量未声明类型,则编译器会通过初始赋值推导变量赋值;当然,未声明类型的变量必需得初始赋值。
1 | // Right |
值类型和引用类型
学过C的都懂。
Go中通过&
运算符来得到变量的地址。
打印
Printf()
函数可以在fmt包外使用,可以向控制台打印格式化字符串,同C语言的printf()
和Go的fmt.Sprintf()
。
fmt.Print()
和fmt.Println()
的作用一致,使用%v对字符串进行格式化,fmt.Println()
会多打印一个\n。
:= 初始赋值运算符
可以使用:=
来高效地进行变量声明和初始化。
var num = 10
就可以简写为num := 10
。
注意: 对同一个变量只能使用一次:=
运算符,否则会报错;对已声明但未使用的局部变量也会出现报错;在变量声明前使用变量也会报错。
变量类型
- bool
- int/uint/uintptr
- int/uint 长度与操作系统位数相等, int16/int32/int64间不能隐式转化,常量除外。
- uintptr 长度足够存放一个指针。
- float32/float64 (没有float和double!!! 尽量用float64)
- byte (int8的别名,字符), 自行了解utf8包。
- string (Go中字符串没有以’\0’), 自行了解strings和strconv包。
- ptr (Go中指针运算是非法的)
变量运算
运算符同C语言;唯一需注意的是带有++
、--
运算符只能当作语句,像sum = i++
这种语句在Go中不合法。
符号优先级优先级 运算符
1 | 7 ^ ! |
控制结构
if-else 结构
1 | if condition1 { |
注意:
- 花括号{}无论如何都不能省略
- {与关键字同一行,}也与关键字同一行
- 条件中可包含初始化语句
switch 结构
1 | // switch 的第一种形式 |
for 结构
- 基于计数器的for语句结构:
for 初始化语句; 条件语句; 修饰语句 {}
- 基于条件判断的for语句:
for 条件语句 {}
- 无限循环:
for {}
- for-range结构:
for ix, val := range coll { }
, 注意val只是对coll中值的拷贝
goto 结构
配合标签使用,不建议使用goto语句
函数
介绍
Go中一共有三种函数:
- 具名函数
- 匿名函数 (Lambda)
- 方法
而且Go允许一个函数A作为另个函数B的参数传入,只要A的返回值数量和类型与B函数参数一致
Go 禁止函数重载
Go中申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体,如func flushICache(begin, end uintptr)
参数和返回值
传参
传参有两种方式:
- 按值传参
- 引用传参
推荐使用引用传参,一般比按值传参有更小的性能开支
具有多个参数的函数也能直接传入一个包含多个变量的slice作为参数
返回值
返回值有一些特性
- 可命名可匿名,使用多个匿名返回值或单个及其以上的命名返回值时需要用()括起来
- 使用命名返回值则return语句可不带参数
1 | package main |
空白符
空白符_
,用于匹配不需要的函数返回值
变长参数
变长参数类型用...type
表示,如func myFunc(a, b, arg ...int) {}
,该类型和slice很像,可用for迭代。
defer与追踪
defer 关键字允许我们在函数返回返回值前才执行某些语句。defer 与 return之间执行顺序是: 先为返回值赋值,再执行defer,最后返回返回值。
1 | package main |
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)
这样我们就能通过defer关键字实现代码追踪,如
1 | package main |
内置函数
详情见 内置函数
递归函数
基本与C语言一致。Go语言中允许相互调用的递归函数,这些函数的声明顺序是随意的。
闭包
这里放个例子:
1 | package main |
数组
声明
数组声明格式
1 | var identifier [len]type |
像让数组接收任意类型的元素需要使用空接口作为类型
Go 中数组是值类型(不是指针),故可通过new()
创建,如new([5]int)
。
通过var
和new()
分别创建的数组arr1和arr2的区别是:
- arr1类型是*[5]int
- arr2类型是[5]int
数组常量
有三种形式:
[5]int{1,2,3,5,6}
[...]int{1,2,3,4,6}
[5]int{3: 4, 5: 6}
多维数组
[4][4]int
数组传递给函数
func demo(a *[3]int){}
数组相等
数组相等判断规则:
1 | v1 := [3]string{"Golang", "Python", "C++"} |
切片
切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。
1 | // Equal Expression |
cap() –数组容量
切片是长度可变的数组, 0 <= len(s) <= cap(s)
,其中cap()
用于返回s切片的容量。多个相关的切片是共享数据的。
切片传递给函数
func demo(a []int){}
make() 创建切片
var slice []type = make([]type, len, [cap])
new和make的区别:
- new是分配新的空间,返回的是指针
- make是返回初始值,只使用于数组,map和channel
bytes 包
提供操作[]byte
类型方法的包
for-range
for-range句式用于遍历slice,其中ix为索引,value为索引值。
1 | for ix, value := range slice1 { |
复制与追加
1 | slice1 := make([]int, 10) |
sort 包
Go提供了sort包来实现各种切片的排序,如sort.Ints(a []int)
, sort.Strings(s string)
Map
Map 声明格式
Map的声明格式为var m map[keytype]valuetype
keytype 只能为简单类型如int
,string
, float
等,切片和结构不能作为keytype,未初始化的值为nil
。
用make()
初始化map变量,而不是用new()
1 | package main |
查键和删键
用val, isPresent = m["key"]
中的isPresent
可以判断键值key
是否存在。
用delete(m, key)
可以删键,且key不存在也不会报错
Map与for-range
1 | for key, value := range m { |
Package
标准库
如fmt
,os
常用功能的150+内置包称作标准库。常见标准库看这里
regexp
有
Match
方法:ok, _ := regexp.Match(pat, searchIn)
同样有
MatchString
方法创建正则对象:
re, _ = regexp.Compile(pat)
替换字符串:
str := re.ReplaceAllString(pat, new)
按函数替换字符串:
str := re.ReplaceAllStringFunc(pat, f)
锁和sync
为避免同一变量同时被不同线程访问修改造成资源竞争,我们需要对变量上一个线程锁
1 | import "sync" |
还有RWMutex
锁,使用RLock()
来允许同时有多个线程对变量进行读操作,只有一个线程进行写操作
精密计算big包
Go提供了big
包来进行精确计算。
big.NewInt(n)
big.NewRat(N, D)
N为分子,D为分母
自定义包
包文件名应由短小的不含_
的单词组成
想使用包得先通过import
关键字导入包: import "relative path or URL"
import "package"
使用包的全局变量和函数需通过
包名.val
和包名.func
import . "package"
可直接使用包的全局变量和函数
import _ "package"
只导入包的副作用,即调用init函数和初始化全局变量
import alias "package"
导入包并使用
alias
别名
格式如下:
1 | // pack_demo包 |
1 | // 主函数入口 |
结构与方法
结构体定义
1 | type identifier struct { |
声明结构体变量
1 | var s T |
值得注意的是,Go中无论是结构体变量还算结构体指针都通过.
选择器符来引用结构体字段。
更简短的初始化结构体实例为
1 | demo := &struct1{10, 1.5, "no"} // 本质是使用new() |
Go中结构体的内存分布是连续块存在的。
结构体中的转化只存在于有相同底层类型的结构体间,而且得通过显式转化。
工厂方法
1 | func NewFile(fd int, name string) *File { |
在Go中,标识符以大写字符开头的能被外部包的代码所引用,否则对外部包不可见。通过这个能使一个结构体设置为私有类型,只能通过工厂方法创建和操作结构体。
通过make()
创建结构体会报错。
结构体中的标签
用于自省类型、文档标记等。可通过reflect
包来获取。
1 | package main |
匿名字段和内嵌结构体
匿名字段可用于模拟结构体的继承,即通过内嵌匿名结构体来模拟。
1 | package main |
命名冲突时:
- 外层名字会覆盖内层名字
- 同一级别出现两次相同的名字会报错
方法
Go 中方法是作用在receiver上的函数。定义格式如下:
1 | func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... } |
无论recv是实例还是指针,都通过object.funcName()
调用方法。
不需要recv的值可以用_
代替:func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
值得注意的是类型和作用其上的方法必需在同一包中,这也是为什么不能在int,float等类型上定义方法。
结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。
类型的String()方法
为结构体定义String()
方法可以使fmt.Printf()
的%v输出或fmt.Print()
和fmt.Println()
的默认输出
垃圾回收和SetFinalizer
runtime.GC()
可以显式调用垃圾收集器
接口
接口定义
类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。
实现某个接口的类型(除了实现接口方法外)可以有其他的方法。
一个类型可以实现多个接口。
接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)
1 | package main |
类型断言
检查断言的方法:
1 | // varI 是接口类型变量 |
也可以用Go中特有的type-switch
语句来判断:
1 | switch t := varI.(type) { // 这里的type画重点!! |
具体类型是值类型还算引用类型取决于该类的接口实现使用的接收者类型(雾
判断某个值是否实现某个接口也可以用varT.(I)
的方法来判断,这里不多赘述
Go 语言规范定义了接口方法集的调用规则:
- 类型 *T 的可调用方法集包含接受者为 *T 或 T 的所有方法集
- 类型 T 的可调用方法集包含接受者为 T 的所有方法
- 类型 T 的可调用方法集不包含接受者为 *T 的方法
空接口
任何类型都实现了空接口,所以可以这样做:
1 | type Any interface {} |
将切片数据复制到空接口切片只能通过for-range
语句显式赋值
读写数据
读取用户输入
fmt.Scanln(&str1, &str2, ...)
从标准输入依次读取以空格分割的字符串,直到遇到\n
fmt.Scanf(format, &str1)
fmt.Sscan(str, &str1, &str2, ...)
从字符串读取
还能用bufio包提供的缓冲读取来实现读取输入。
1 | import ( |
文件读写
通过os
包的Open()
函数能得到文件的句柄
1 | // import ("os", "bufio") |
还可以用io/ioutil
将整个文件内容读到一个字节切片([]byte
)中,
1 | // import ("io/ioutil") |
带缓冲的读取
1 | buf := make([]byte, 1024) |
Fscanln
读取文件数据
1 | // inputFile --- file handler |
切片读文件
1 | // n是字符数, f是文件句柄, buf是读取到文件内容的切片 |
写文件:
1 | file, err := os.Open("data.txt", os.O_WRONLY|os.O_CREATE, 1024) |
使用io
包的Copy(dst, src)
函数可以实现文件的拷贝,其中dst和src是文件句柄。
命令行参数
通过os.Args
可以获取命令行参数的切片(os.Args[0]
是程序名)
flag
包也能处理命令行参数:
1 | import ( |
JSON数据
Go中用encoding/json
包来处理json数据
Go结构体 => JSON
1 | import ( |
Go中与JSON对应的数据结构:
- bool 对应 JSON 的 booleans
- float64 对应 JSON 的 numbers
- string 对应 JSON 的 strings
- nil 对应 JSON 的 null
编码Map对象需要是map[string] T类型
Channel,复杂类型不能被编码
map[string]interface{}和[]interface{}能解码任何JSON对象和数组
错误处理
定义错误
Go中提供了errors
包来定义错误
1 | import ( |
也可以用fmt.Errorf()
创建错误对象
1 | err := fmt.Errorf("usage: %s", something) |
运行时异常
Go中用panic
产生运行时异常
1 | func ReadInfo() { |
从panic恢复
在多次嵌套调用的函数中触发panic使defer 语句保证执行并且控制权交给panic调用的函数。栈会被展开直到defer语句中的recover()被调用
1 | func protect(g func()) { |
所以我们就能用闭包处理错误
1 | import ( |
测试
单元测试和基准测试
测试代码的包文件名满足这种形式*_test.go
,且必需导入testing
包,写一些Test*开头的全局函数
1 | func TestAbcde(t *testing.T) |
一些二通知测试失败的函数:
1 | func (t *T) Fail() // 标记测试函数为失败,然后继续执行(剩下的测试)。 |
使用go test
来编译测试程序,并执行所有Test*的函数,所有函数通过会打印PASS
做简单的基准测试需要测试代码中包含Benchmark的函数并接收一个testing.B类型的参数。
1 | func BenchmarkReverse(b *testing.B) { |
命令 go test –test.bench=.* 会运行所有的基准测试函数;代码中的函数会被调用 N 次(N 是非常大的数,如 N = 1000000),并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。
表驱动测试
1 | var tests = []struct{ // Test table |
协程与通道
并发、并行和协程
- 一个应用程序是运行在机器上的一个进程,进程是一个运行在自己内存地址空间里的独立执行体。
- 一个进程由一个或多个操作系统线程组成,线程其实是共享同一个内存地址空间的一起工作的执行体。
- 协程运行在线程之上,协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程。
- Go使用
go
关键字就能开启协程,要注意若主线程先结束,未结束的协程也会中断。
GOMAXPROCS
用GOMAXPROCS 为一个大于默认值 1 的数值来允许运行时支持使用多于 1 个的操作系统线程,否则所有的协程都会共享同一个线程。
协程的数量 > 1 + GOMAXPROCS > 1。
1 | runtime.GOMAXPROCS(2) |
channel
channel类型本质是队列。声明格式为var identifier chan datatype
,之后必需实例化它:identifer = make(chan datatype)
用var identifier <- chan datatype
声明只读通道, var identifier chan <- datatype
声明只写通道。
通道操作符为<-
:
1 | ch := make(chan int) |
通道阻塞: 当通道数据已满且没有接收者读出数据/通道已空仍有接收者尝试读出数据,就会触发通道阻塞,直到数据被读出/通道读入新数据
遍历channel:
1 | ch := make(chan int, 100) |
可以用select语句切换协程
1 | select { |
sync包的WaitGroup
WaitGroup 用于创建任务队列,其中提供了三个方法WaitGroup.Add(count)
、WaitGroup.Done()
、WaitGroup.Wait()
1 | package main |