《Go语言编程》阅读笔记(1) 基本的语法和语言特点
初识Go
静态语言
语言特性:
自动垃圾回收
内置类型丰富
多返回值的函数
错误处理:关键字defer、panic、recover
匿名函数、闭包——函数可作为参数传递
1
2
3f := func(x, y int) int {
return x+y
}并发编程:每个用关键字go执行的函数可以运行成为一个单位协程,一个协程阻塞,调度器把其他协程安排到另外的线程执行;用通信顺序进程CSP作为goroutine间的通信方式,用channel实现CSP(类似管道pipe)
类和接口
反射:获取对象类型的详细信息,需要import reflect
通过Cgo重用现有的C模块,利用Cgo的特定语法混合C代码
GOPATH和PATH环境变量类似,能接受多个路径
源码开头为package声明,表明该源码所在的包,包是基本的分发单位;生成Go的exe,必须建立名为main的包,其中包含main函数,main()无参数,无返回值,命令行参数保存在os.Args,flag包用于规范命令行参数并获取、解析命令行参数
函数:返回值没有明确赋值时将被设置为默认值
1
2
3func 函数名(参数列表)(返回值列表){
//参数1 参数1数据类型, 参数2 参数2数据类型, ...
}注释格式同C++
左边花括号不允许另起一行
问题追踪:
fmt包提供打印函数,其中Printf与PrintIn()类似于C的Printf()
log包:日志功能
go内置了print函数,但属于输出到标准错误流中并打印
顺序编程
变量
变量声明:关键字var,变量类型在变量名后;若干声明的变量可以放置在一起
1 | var ( |
变量初始化:可以删去var,
var v1 int = 10
和var v2 = 10
和v3 := 10
是相同的,符号:=表示变量声明和初始化同时进行,此时左边的变量不能是被声明过的go的变量声明后必须使用
可以有
i,j=j,i
函数可以有多个返回:
常量
字面常量:无类型,只要在相应类型的值域里就可以作为该类型的常量,如-12可以赋值int、uint、int32、float64、complex64等类型的变量
可以通过const关键字定义,可以不限定常量类型
三个预定义常量:true、false、iota;iota在每一个关键字出现时被重置为0,下一个const出现前每出现一次iota,其对应数字自动加一
枚举一系列的常量
1
2
3
4const (
Friday = iota
number_of_days
)大写字母的开头的常量在包外可见
数据类型
bool:
var v1 := (1==2)
,不接受其他类型赋值,不支持自动和强制类型转换整型:int和int32是不同类型,需要强制转换
value2 = int32(value1)
(存在精度损失、值溢出的问题)支持+-*/和%运算,支持比较运算(同C),不同类型的整型数不能直接比较,会编译报错,但可以直接和字面常量比较
1
2
3
4
5var i int32 = 1
var j int64 = 2
if i == 1 || j == 2 {
...
}位运算和C类似,取反为
^x
float64相当于double,
value1 := 12.0
,浮点数的比较建议使用:1
2
3
4
5import "math"
// p 为自定义的比较精度,如0.0001
func IsEqual(f1, f2, p float64){
return math.Fdim(f1, f2) < p
}复数:
var value1 complex64 = 3.2 + 12i
——两个float32构成,通过real()
和imag()
获得实部和虚部字符串:
var a string = "abc"
,通过数组下标获取内容,不能在初始化后修改,通过len()
获取长度,字符串连接用运算符+支持两个字符类型:byte和rune(代表单个Unicode字符),go的多数api假设字符串为utf-8编码
数组声明方法:
* 遍历:`for i:=0; i< len(array); i++ {}`或`for i, v := range array {}`——range:索引+元素值,类似enumeratego中数组为值类型(value type),所有值类型变量赋值和参数传递时产生一次复制——数组作为函数参数时,会复制一次,函数体无法修改外面数组内容
修改方法:数组切片,数组切片分为三个变量,一个指向原生数组的指针,数组切片中元素个数,数组切片已分配的存储空间;数组切片仍使用数组管理元素(类似C++中vector和数组的关系)
根据一个数组创建数组切片:
根据
make()
创建数组切片:(make只能为slice, map, channel分配内存)操作数组的所有方法都适用于数组切片
cap()
返回数组切片分配空间大小,len()
返回数组切片当前元素数目append()
:slice = append(slice, 1, 2, 3)
,尾部加上3个元素,生成一个新的数组切片;也可slice = append(slice, slice2...)
(slice2后面有3个点,不能省略)基于数组切片创建数组切片,只要选择范围不超过
cap(oldslice)
即可,多余位置补01
2oldslice := []int {1,2,3}
newslice := oldslice[:5]copy(slice1, slice2)
:按较短的数组切片元素个数进行复制
map:类似dict,键值对的未排序组合
- 其中的string为键的类型,后面PersonInfo为值类型
- 通过
make
创建一个新的map,可以创建时指定其初始存储能力:cur_map := make(map[string] int, 100)
,或者创建并初始化一个map:tmp_map = map[string] int{"abc": 1}
- 通过
delete(cur_map, key1)
删除键为key1的键值对,若不存在也不报错
流程控制
条件语句:类似C,不允许将最终的return语句包含在if else结构中,花括号必须存在
选择语句:不需要break明确跳出一个case,只有在case中添加fallthrough关键字,才会执行下一个case;若不设定switch后的条件表达式,则整个switch类似于多个if else;
循环语句只有for,用下面的写法替代while;不支持以逗号间隔的多个赋值语句;break、continue类似C,但break可以选择中断哪个循环
goto:跳转到本函数内的某个标签
函数
若参数列表中若干个相邻参数的类型相同,则参数列表中额可以省略前面变量的类型声明
1
2
3
4
5
6func Add(a, b int)(ret int, err error) {
...
}
func Add(a, b int) int {
//如果只有一个返回值
}调用:如果导入过该函数所在的包,则:
c := math.Add(1, 2)
Go中函数名字大小写体现了该函数的可见性,有时导入了包,但编译器仍会报错无法找到
add_xxx
函数——大写字母开头的函数才能被其他包使用,此规则同样适用于类和变量的可见性不定参数:函数传入的参数数目不确定(下例中,参数类型都是int),形如
...type
格式的类型只能作为参数类型且为最后一个参数,本质上是一个数组切片。如果希望传递任意类型,则为...interface{}
1
2
3
4
5
6
7
8func tmp1(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
func tmp2(format string, args ...interface{}) {
//...
}可以给返回值命名,其值在函数开始时自动初始化为空:
func tmp()(n int, err Error){}
匿名函数:不定义函数名,可以直接赋值给一个变量或直接执行;go的匿名函数是一个闭包
1
2
3
4
5
6
7f := func (a, b int, z float64) bool {
return a*b < int(z)
}
fmt.Println(f(1, 2, 3.5))
func (ch chan int) {
ch <- ACK
}(reply_chan) // 花括号直接跟参数列表,表示函数调用错误处理
error接口:
接口定义为:
对于函数,如果要返回error,error通常作为多种返回值的最后一个
举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string{
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
func Stat(name string) (fi FileInfo, err error){
var stat syscall.Stat_t
err = syscall.Stat(name, &stat)
if err != nil{
return nil, &PathError("stat", name, err)
}
return fileInfoFromStat(&stat, name), nil
}
defer:例子如下,一个函数可以存在多个defer语句,defer的调用服从先进后出
panic()和recover():报告和处理运行时的错误:
panic()调用时,正常执行流程停止,函数中defer关键字延迟执行的语句开始执行,函数返回到调用它的位置,并逐层向上执行panic流程,直到所属的goroutine中所有在执行的函数终止。报告错误信息,包括调用panic()时传入的参数——此过程称为错误处理流程
recover()用于终止错误处理流程,一般应该在一个使用了defer的函数中执行
实例
结构如下,sorter.go:package main,
import "algorithm/bubblesort"
,import "algorithm/qsort"
;bubblesort.go:package bubblesort;bubblesort_test.go:单元测试文件,package bubblesort;qsort.go和qsort_test.go类似flag包解析命令行参数(一些变量可以定义在函数外,作为全局变量,这里*string为指针,类似C++,此时如果要在另一个函数内部修改数组,直接传递指针或数组切片即可)
文件读取操作,需要导入os包打开文件,
file, err = os.Open(infile)
,文件关闭需要用defer file.close()
,然后通过bufio包读取文件内容文件输出操作,先生成输出文件
file, _, _ = os.Create(outfile)
,之后file.WriteString(string1)
strconv包实现基本数据类型与字符串表示的转换,
strconv.Itoa(x int) string
:接收一个int类型参数,返回x的字符串(反之为Atoi
)单元测试文件一般需要导入testing包,一个样例如下
面向对象
类型系统
可以给任意类型(包括内置类型)添加新的函数,以int为例,定义一个新的类型Integer(就是int)并增加一个新的函数Less(),此时若
var a Integer = 1
有a.Less(2)
Go中没有类似python的self或类似C++的this;上例中,如果要修改对象,可以改用指针:
http包中关于HTTP头部信息的Header类型和上例也类似:
值语义和引用语义:
- 对于
b=a
,如果b的修改不影响a的值,则此类型为值类型。go中大多数类型为值类型,包括:基本类型、数组array(这和C++完全不同)、结构体struct、指针等,若要表达引用,必须:var b = &a
- 有4个类型具有引用含义:数组切片(指向数组的一个区间)、map、channel、interface
- 对于
结构体:和其他语言的class具有同等地位,但放弃了很多面向对象特性,只保留了组合特性。例如,定义一个矩形类型,并定义一个成员方法计算矩形的面积(Rect为类的名称,该类是一个struct)
1
2
3
4
5
6
7
8
9
10type Rect struct {
x, y float64
width, height float64
}
func (r *Rect) Area() float64 {
return r.width * r.height
}
...
tmp := &Rect{x: 1, y: 2, width: 2, height: 2}
fmt.Println(tmp.Area())
初始化
初始化方式如下,没有显式初始化都会被初始化为该类型的零值
go没有构造函数,对象的创建通常由一个全局的创建函数完成(以NewXXX命名):
匿名组合
以匿名组合的方式实现继承,如下例,定义Base类,有两个成员函数Foo()和Bar(),定义一个Foo类,继承并改写了Bar()方法
1
2
3
4
5
6
7
8
9
10
11
12type Base struct {
Name string
}
func (base *Base) Foo() {...}
func (base *Base) Bar() {...}
type Foo struct {
Base
}
func (foo *Foo) Bar() {
foo.Base.Bar()
...
}派生类Foo没有改写即类的成员方法时,相应的方法就被继承,例如foo.Foo()和foo.Base.Foo()效果相同
可以以指针的方式得到“派生类”,此时Foo创建实例时,需要提供一个Base实例的指针——类似于C++的虚基类
1
2
3type Foo struct {
*Base
}
可见性
- 没有private、public、protected这些关键字,若某个符号对其他package可见,则该符号以大写字母开头,成员函数的可访问性类似
- Go语言符号的可访问性是包一级,即包内部的其他类都可以访问它
接口(很重要)
go之前,其他语言的接口在实现类时,需要明确声明自己实现了某个接口,即强制性接口继承,称为侵入式接口
go中,一个类只要实现了接口要求的所有函数,则称这个类实现了该接口,例如定义一个File类,实现了Read、Write、Seek、Close函数,并定义IFile、IReader、IWriter、ICloser接口。
File没有从这些接口继承,但File实现了这些接口,因此可以进行赋值
此时不再需要绘制类的继承图,并且实现类的时候只需要关心类提供了什么方法,不需要考虑接口要拆分多细才合理,还不用为了实现一个接口而导入一个包
接口赋值:包括将对象实例赋值给接口,或者将一个接口赋值为另一个接口
前者,要求对象实例实现了接口要求的所有方法,例如,定义一个Integer类型,定义接口LessAdder:
1
2
3
4
5
6
7
8
9
10
11
12
13type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
var a Integer = 1
var b LessAdder = &a- 此时*Integer既存在Less函数,又存在Add()函数,满足LessAdder接口——Go会根据
func (a Integer) Less(b Integer) bool
自动生成新的方法func (a *Integer) Less(b Integer) bool {return (*a).Less(b)}
- 此时*Integer既存在Less函数,又存在Add()函数,满足LessAdder接口——Go会根据
后者,只要两个接口具有相同的方法列表,它们就是等同的,如果接口A的方法列表是接口B的方法列表的子集,则接口B可以赋值给接口A,反之不行
接口查询:查询Writer接口能否转化为IStream接口;进一步可以查询接口只想的对象是否为某个类型(file1接口指向的对象实例是否为*File类型——前文里,接口的赋值使用了&)
1
2
3
4
5
6
7var file1 Writer = ...
if file5, ok := file1.(IStream); ok {
...
}
if file6, ok := file1.(*File); ok {
...
}类型查询:查看接口指向对象实例的类型,例如:
接口组合:
Any类型:任何对象实例都满足空接口interface{},因此当函数可以接收任意的对象实例时,通常会声明为interface{}(
func Println(args ...interface{})
)
实例
- go标准库中没有GUI相关的功能,也没有成熟的第三方界面库——go初始定位为高并发的服务器端程序
- 略