理解go语言编程-顺序编程

Go语言中提供了C/C++程序员期盼多年的多重赋值功能,比如下面这个交换i和j变量的语句:
i, j = j, i
在不支持多重赋值的语言中,交互两个变量的内容需要引入一个中间变量:
t = i; i = j; j = t;

Go的常量定义const Pi float64 = 3.14159265358979323846可以限定常量类型,但不是必需的。如果定义常量时没有指定类型,那么它 与字面常量一样,是无类型常量const zero = 0.0。
由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达式,比如试图以如下方式定义常量就会导致编译错误:const Home = os.GetEnv(“HOME”)

iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const (
a = 1 << iota // a == 1 (iota在每个const开头被重设为0)
b = 1 << iota // b == 2
c = 1 << iota // c == 4
)

// 如果两个const的赋值语句的表达式是一样的,那么可以省略后一个赋值表达式
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 // c1 == 1
c2 // c2 == 2
)

// 枚举的写法
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberOfDays // 这个常量没有导出
)

布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。

1
2
3
var b bool
b = (1!=0) // 编译正确
fmt.Println("Result:", b) // 打印结果为Result: true

注意,:= 声明与定义只能放在函数中,或者使用var。

1
2
3
4
var item = &memcache.Item {
Key: "lyric",
Value: []byte("Oh, give me a home"),
}

int和int32在Go语言里被认为是两种不同的类型,编译器也不会帮你自动 做类型转换

1
2
3
var value2 int32
value1 := 64 // value1将会被自动推导为int类型 value2 = value1 // 编译错误
value2 = int32(value1) // 编译通过

两个不同类型的整型数不能直接比较,比如int8类型的数和int类型的数不能直接比较,但各种类型的整型变量都可以直接与字面常量(literal)进行比较.

取反在C语言中是~x,而在Go语言中是^x.

float32等价于C语言的float类型, float64等价于C语言的double类型

fvalue2 := 12.0 其类型将被自动设为float64,而不管赋给它的数字是否是用32位长度表示的

1
2
3
4
// p为用户自定义的比较精度,比如0.00001
func IsEqual(f1, f2, p float64) bool {
return math.Fdim(f1, f2) < p
}

复数的表示,对于一个复数z = complex(x, y),就可以通过Go语言内置函数real(z)获得该复数的实部,也就是x,通过imag(z)获得该复数的虚部,也就是y

1
2
3
4
var value1 complex64
value1 = 3.2 + 12i // 由2个float32构成的复数类型
value2 := 3.2 + 12i // value2是complex128类型
value3 := complex(3.2, 12) // value3结果同 value2

字符串遍历,一种是以字节数组的方式遍历,每个中文字符在UTF-8中占3个字节,而不是1个字节。另一种,以Unicode字符方式遍历时,每个字符的类型是rune(早期的Go语言用int类型表示Unicode字符),而不是byte。

1
2
3
4
5
6
7
8
9
10
11
str := "Hello,世界"
n := len(str)
for i := 0; i < n; i++ {
ch := str[i] // 依据下标取字符串中的字符,类型为byte,实际上是uint8的别名
fmt.Println(i, ch)
}

str := "Hello,世界"
for i, ch := range str {
fmt.Println(i, ch) // ch的类型为rune,代表单个Unicode字符
}

数组声明方法

1
2
3
4
5
[32]byte // 长度为32的数组,每个元素为一个字节
[2*N] struct { x, y int32 } // 复杂类型数组
[1000]*float64 // 指针数组
[3][5] int // 二维数组
[2][2][2]float64 // 等同于[2]([2]([2]float64))

在Go语言中数组是一个值类型(value type)。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,在函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func modify(array [10]int) {
array[0] = 10 // 试图修改数组的第一个元素
fmt.Println("In modify(), array values:", array)
}
func main() {
array := [5]int{1,2,3,4,5} // 定义并初始化一个数组
modify(array) // 传递给一个函数,并试图在函数体内修改这个数组内容
fmt.Println("In main(), array values:", array)
}

数组切片的数据结构可以抽象为以下3个变量:

  • 一个指向原生数组的指针;
  • 数组切片中的元素个数;
  • 数组切片已分配的存储空间。

数组切片本质上是一个区间,你可以大致将[]T表示为:

1
2
3
4
5
6
// 数组切片内部是指向数组的指针,所以可以改变所指向的数组元素
type slice struct {
first *T
len int
cap int
}

并非一定要事先准备一个数组才能创建数组切片。Go语言提供的内置函数make()可以用于灵活地创建数组切片,事实上还会有一个匿名数组被创建出来,只是不需要我们来操心而已。

1
2
3
mySlice1 := make([]int, 5) // 创建一个初始元素个数为5的数组切片,元素初始值为0
mySlice2 := make([]int, 5, 10) // 创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间
mySlice3 := []int{1, 2, 3, 4, 5} // 接创建并初始化包含5个元素的数组切片

可动态增减元素是数组切片比数组更为强大的功能。与数组相比,数组切片多了一个存储能力(capacity)的概念,即元素个数和分配的空间可以是两个不同的值。合理地设置存储能力的 值,可以大幅降低数组切片内部重新分配内存和搬送内存块的频率,从而大大提高程序性能。
cap()函数返回的是数组切片分配的空间大小,而len()函数返回的是数组切片中当前所存储的元素个数。

1
2
3
4
mySlice = append(mySlice, 1, 2, 3) // 从尾端给mySlice加上3个元素
mySlice2 := []int{8, 9, 10}
// 给mySlice后面添加另一个数组切片
mySlice = append(mySlice, mySlice2...) // 加上省略号相 7 当于把mySlice2包含的所有元素打散后传入。

基于数组切片创建数组切片,如下,选择的oldSlicef元素范围甚至可以超过所包含的元素个数,比如newSlice
可以基于oldSlice的前6个元素创建,虽然oldSlice只包含5个元素。只要这个选择的范围不超过oldSlice存储能力(即cap()返回的值),那么这个创建程序就是合法的。newSlice中超出oldSlice元素的部分都会填上0。

1
2
3
oldSlice := []int{1, 2, 3, 4, 5}
newSlice := oldSlice[:3] // 基于oldSlice的前3个元素构建新数组切片
// 注意:前一个元素为oldSlice[:1]

内容复制,如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制。

1
2
3
4
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

在Go中,使用map不需要引入任何库,并且用起来也更加方便。myMap是声明的map变量名,string是键的类型,PersonInfo则是其中所存放的值类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myMap map[string] PersonInfo
// or
myMap = make(map[string] PersonInfo)
// 100 capacity
myMap = make(map[string] PersonInfo, 100)
// init
myMap = map[string] PersonInfo{
"1234": PersonInfo{"1", "Jack", "Room 101,..."},
}
// delete
// 如果“1234”这个键不存在,那么这个调用将什么都不发生,也不会有什么副作用。但是如果传入的map变量的值是nil,该调用将导致程序抛出异常(panic)。
delete(myMap, "1234")
// find
value, ok := myMap["1234"]
if ok { // 找到了
// 处理找到的value
}

在if之后,条件语句之前,可以添加变量初始化语句,使用;间隔;
在有返回值的函数中,不允许将“最终的”return语句包含在if…else…结构中,否则会编译失败

switch中,switch后面的表达式甚至不是必需的,只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case

Go语言中的循环语句只支持for关键字,而不支持while和do-while结构,简化为for表示while。for不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量。for循环同样支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环。

1
2
3
4
5
6
7
8
9
for j := 0; j < 5; j++ {
for i := 0; i < 10; i++ {
if i > 5 {
break JLoop
}
fmt.Println(i)
}
}
JLoop: // ...

go函数如果参数列表中若干个相邻的参数类型的相同,比如上面例子中的a和b,则可以在参数列表中省略前面变量的类型声明

1
2
3
func Add(a, b int)(ret int, err error) {
// ...
}

形如…type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。它是一个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,类型…type本质上是一个数组切片,也就是[]type,这也是为什么上面的参数args可以用for循环来获得每个传入的参数。

1
2
3
4
5
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}

不定参数的传递,使用interface{}仍然是类型安全的,这和C/C++不太一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func myfunc(args ...int) {
myfunc3(args...) // 按原样传递
myfunc3(args[1:]...) // 传递片段,实际上任意的int slice都可以传进去
}

func Printf(format string, args ...interface{}) { // 希望传任意类型
// ...
}

func MyPrintf(args ...interface{}) {
for _, arg := range args {
switch arg.(type) { // 类型查询
case int:
fmt.Println(arg, "is an int value.")
case string:
fmt.Println(arg, "is a string value.")
case int64:
fmt.Println(arg, "is an int64 value.")
default:
fmt.Println(arg, "is an unknown type.")
}
}
}

匿名函数可以直接赋值给一个变量或者直接执行:

1
2
3
4
5
6
f := func(x, y int) int {
return x + y
}
func(ch chan int) {
ch <- ACK
} (reply_chan) // 花括号后直接跟参数列表表示函数调用

error处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type PathError struct {
Op string
Path string
Err error
}
// 编译器又怎能知道PathError可以当一个error来传递呢?关键在于下面的 代码实现了Error()方法
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
// 如果在处理错误时获取详细信息,而不仅仅满足于打印一句错误信息,那就需要用到类型转换知识了
fi, err := os.Stat("a.txt")
if err != nil {
if e, ok := err.(*os.PathError); ok && e.Err != nil {
// 获取PathError类型变量e中的其他信息并处理
}
}

如果觉得一句话干不完清理的工作,也可以使用在defer后加一个匿名函数的做法:

1
2
3
defer func() {
// 做你复杂的清理工作
} ()

defer语句的调用是遵照先进后出的原则,即最后一个defer语句将最先被执行。

异常处理

1
2
func panic(interface{})
func recover() interface{}

一般情况下,recover()应该在一个使用defer关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的goroutine中明确调用恢复过程(使用recover关键字),会导致该goroutine所属的进程打印异常信息后直接退出。

1
2
3
4
5
defer func() {
if r := recover(); r != nil {
log.Printf("Runtime error caught: %v", r)
}
}()

nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!