golang 基础学习笔记(-)———— 基本语法

Go 基础语法

Go 程序的基本构成

Hello world

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("hello, world")
}

Go 程序的基本构成,每个程序都有自己所在的 package, 这个概念类似于其他编程语言的命名空间 或者 库名, 一般用一个简单的单词,与其他功能区分开即可, 命名规范要求全小写,或者'_'连接的多个单词

Go 程序是通过 import 关键字将一组包链接在一起。
import "fmt" 表示该程序需要使用 package fmt(函数或者其他元素),其中 “fmt” 是一个包名

格式是

  • import “package_name”
  • import
    (

    “package_name”

    )

function

Go 程序的重要组成部分是函数,格式是 func 函数名(参数列表) (返回值列表) {}

main.main

Go 程序的入口是 main.main 方法, 第一个main是该程序的 package; 第二个 main 是该程序的 main 函数,main 函数要求没有参数列表和返回值列表。

基础数据类型

int

包含int8, int16, int32, int64,分别8bit, 16bit, 32bit, 64bit的有符号整数,而int类型占的bit数决定于编译环境所在平台的位数,如果是32位环境则是32bit,如果是64位环境则是64bit。
除了有符号数之外还要无符号数,Go 语言中,无符号型整数有 uint8, uint16, uint32, unit64, int8 的取值范围是 [-128~127], 而uint8的取值范围是 [0~255], 其他有符号型和无符号型整数的取值范围和这个类型类似。

byte

byte is an alias for uint8 and is equivalent to uint8 in all ways. It is used, by convention, to distinguish byte values from 8-bit unsigned integer values.

byte 是 uint8 的别名(alias), 或者说完全等同于 uint8。按照惯例用于区分8bit无符号数和byte 类型数据

rune

rune is an alias for int32 and is equivalent to int32 in all ways. It is used, by convention, to distinguish character values from integer values.
rune 是 `int32` 类型的别名, 或者说完全等同于 `int32`. 按照惯例用于区分字符类型和整数类型

rune 用数字表示字符的 ASCII编码

float

浮点类数据类型包含float32float64, Go 语言基于 IEEE 754标准实现的浮点类型数据,具体参见另一篇文章浮点数

bool

布尔类型数据,取值有 true 和 false

complex

复数,有对应float32float64 两个精度的数据类型 complex64complex128,
complex(1, 2)` 等同于 1 + 2i

string

字符串类型,底层实现是byte 类型的 slice,不可变(immutable)

iota

1
const iota = 0

iota 是无类型的int 类型的常量计数器,在const关键字出现时配置为0,从0开始,const 中每增加一行iota 自增一次(+1)

iota 的使用示例如下, 在 const 关键字修饰的括号中, iota 首先被重置成0, 然后 a = 0, b = 1, c = 2

1
2
3
4
5
const (
a = iota // 0
b // 1
c // 2
)

从上面的特性来看,我们可以使用 iota 来定义一个简单的枚举,来个小例子

1
2
3
4
5
6
7
8
type chargeType int

const (
CPC chargeType = iota // 0
CPM // 1
CPT // 2
CPD // 3
)

设想你在处理消费者的音频输出。音频可能无论什么都没有任何输出,或者它可能是单声道,立体声,或是环绕立体声的。
这可能有些潜在的逻辑定义没有任何输出为 0,单声道为 1,立体声为 2,值是由通道的数量提供。
所以你给 Dolby 5.1 环绕立体声什么值。
一方面,它有6个通道输出,但是另一方面,仅仅 5 个通道是全带宽通道(因此 5.1 称号 - 其中 .1 表示的是低频效果通道)。
不管怎样,我们不想简单的增加到 3。
我们可以使用下划线跳过不想要的值。

1
2
3
4
5
6
7
8
9
10
type AudioOutput int

const (
none AudioOutput = iota // 0
mone // 1
stereo // 2
_
_
Dolby // 5
)

控制结构

if-else

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if condition {
// do something
}

if condition {
// do something
} else {
// do something
}

if condition1 {
// do something
} else if condition2 {
// do something
} else {
// do something
}

关键字 if 和 else 之后的左大括号 { 必须和关键字在同一行,如果你使用了 else-if 结构,则前段代码块的右大括号 } 必须和 else-if 关键字在同一行。这两条规则都是被编译器强制规定的。

除了上述情况之外还有如下使用方法

1
2
3
if initialization; condition {
// do something
}

Example:

1
2
3
if i := getValue(); i > 10 {
// do something
}

for

“1. 常见用法

1
2
3
for i := 0; i < count; i++ {
//do something
}
  1. go 中没有while 循环,用for 循环代替
1
2
3
for i < 100 {
//do something
}
  1. 无限循环,没有 condition
1
for {}


1
for index := 0; ; index ++ {}

  1. for range
1
2
3
for idx, element := range intArr { // idx 是数组下表,element 是数组下标所在元素
//do something
}


1
2
3
for idx := range intArr { // idx 是数组下表
//do something
}

注意:for a := range arr 这种遍历的时候 a 是数组下标


1
2
3
for key, value := range map1 { // Key map 中的key value 是map 中的值
//do something
}

  1. breakcontinue
1
2
3
4
5
for idx := 0 ;; idx ++ {
if idx > 10 {
break; // 退出当前for 循环
}
}
1
2
3
4
5
for idx := 0 ;; idx ++ {
if (idx % 2) == 0 {
continue; // 进入下一个循环
}
}

switch case

相比其他语言来说go 语言的 switch case 更强大,它支持各种形式的表达式,而且匹配上一个 case 执行完分支代码后,程序会自动switch代码块,不需要使用 break 标明结束

1
2
3
4
5
6
7
8
switch var1 {
case val1:
// do something
case val2:
// do something
default: // 不符合之前所有已给出条件的时候走到这里,建议写到最后
// do something
}

上面例子中var1 可以是任何类型, 而 val1val2 是同类型的任意值

多个case 合并

1
2
3
4
5
6
7
8
switch i {
case 1, 2:
//do something
case 3:
//do something
default:
//do something
}

or

1
2
3
4
5
6
7
8
9
switch i {
case 1:
case 2:
//do something
case 3:
//do something
default:
//do something
}

fallthrough

当执行完一个 case 分支后还想继续执行下一个 case 分支,可以使用关键字 fallthrough

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
var i = 2
switch i {
case 0:
case 1:
fmt.Printf("Value 1 or 0, val:%v \n", i)
case 2:
fmt.Printf("Value 2, val:%v \n", i)
fallthrough
case 3:
fmt.Printf("Value 3, val:%v \n", i)
default:
fmt.Printf("Value default, val:%v \n", i)
}
}
`

上面程序的输出结果是:

Value 2, val:2
Value 3, val:2

Process finished with exit code 0

array 数组

数组是固定长度的同一类型元素组成的序列,可以通过数组下标访问来访问元素,下标从0开始

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型,数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

golang 提供内建函数 append(arr []Type, element... Type) 用于往数组中添加元素,也可用于 slice
数组相关的内建函数 还有 len()cap(), 对于数组而言 len()cap() 是一样的

len() 函数的参数也可以是 nil, len(nil) == 0

数组的声明格式是

1
var arr [length]Type

刚声明的数组长度是 length,每个元素都是零值(数字是0, 字符串是’’,引用类型是nil)
也可以使用 make 定义一个数组

1
var arr = make([]Type, len)

还可以在定义的是时候设定初始值

1
var arr = [3]int64{1,2,3}

或者设定指定下标的元素

1
var arr = [3]int64{2:1000}  // arr := []int64{0, 0, 1000}

如果数组元素本身可以比较的,那么数组也是可以用 == 比较的,反则会编译失败

slice(切片)

slice(切片) 是对数组的一个连续片段的使用

切片也是可索引的,也可以使用 len() 函数的来获取长度
切片除了长度之外,还有一个属性是容量, 通过内建函数 cap() 函数来获取容量,容量的含义是切片开始位置到底层数组结束位置的数组长度
例如切片 s 是数组 a 的一部分,s = a[3:], 那么 cap[s] = len(a) - 3
对于切片来说,始终需要保证 0 <= len(s) <= cap(s), 如果 len(s) > cap(s) 就会出现越界异常

从上面这段文字总结出 slice 的组成元素是:
“1. 指针(指向底层数组中切片的第一个元素)

  1. 长度
  2. 容量

下图描述了切片的构成
图片

slice 的初始化方式是

1
2
3
4
5
6
7
8
var slice1 [...]Type    // 不需要指明长度,编译器会自动构建一个长度合适的底层数组
var slice2 []Type // _..._可以不写
var arr [length]Type // 声明一个数组
slice3 := arr[start:end] // 声明一个由数组中 [start, end) 元素组成的切片
slice4 := arr[start:] // end 可以省略不写,表示 end = len(arr)声明一个由数组中 [start, len(arr)) 元素组成的切片
slice5 := arr[:end] // start 可以省略不写,表示 start = 0 声明一个由数组中 [0, end) 元素组成的切片
slice6 := make([]Type, len, cap) // 声明一个具体类型,长度是len,容量是cap 的切片, make 关键字适用于 array, slice, map, channel 的内存分配
slice7 := new([]Type) // 也可以用 new 关键词定义一个 len == cap == 0 的slice,但是返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。强烈建议不要使用 new 关键字声明 slice

由于slice 是共享的底层数组,当一个slice 改变了底层数组的时候,和它共享底层数组的其他slice 也会受影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
var arr [6]int64
for idx := range arr {
arr[idx] = int64(idx + 1)
}
fmt.Printf("The underlying array:%v \n", arr)

s := arr[3:5]
fmt.Printf("The slice:%v, length:%v, capacity:%v \n", s, len(s), cap(s))

s[0] = 1000
fmt.Printf("The underlying array after set value for slice :%v \n", arr)
fmt.Printf("The slice after set value:%v \n", s)
}

输出结果如下:

The underlying array:[1 2 3 4 5 6 7 8 9 10]
The slice 1:[4 5 6 7 8 9 10], length:7, capacity:7
The slice 2:[5 6 7 8 9 10], length:6, capacity:6
The underlying array after set value for slice :[1 2 3 4 5 1000 7 8 9 10]
The slice 1 after set value:[4 5 1000 7 8 9 10]
The slice 2 after set value:[5 1000 7 8 9 10]

如果不想共享底层数据,可以使用内建的 copy(resourceSlice, targetSlice)函数 从原数组或者切片中拷贝一个新的切片,然后对新的切片的操作就不会因为共享底层数组影响其他切片了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第一个集合中排除掉第二个集合后的结果
func Subtract(c1 []interface{}, c2 []interface{}) (subtraction []interface{}, err error) {
subtraction = make([]interface{}, len(c1))
copy(subtraction, c1) // copy c1 to subtraction
subtraction = c1[:]
for _, elem2 := range c2 {
for idx, elem1 := range subtraction {
if elem2 == elem1 {
subtraction = append(subtraction[:idx], subtraction[idx+1:]...)
}
}
}
return
}

在从数组或切片中生成新的切片的时候还可以指定新切片的容量
语法是

1
newSlice := arr[start:end:cap]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func testSliceCap() {
a := []int32{1, 2, 3, 4}

s1 := a[2:]
fmt.Printf("a: %v, s1: %v\n", a, s1)

s2 := a[0:2:2]
fmt.Printf("a: %v, s2: %v\n", a, s2)
s2 = append(s2, 100)
fmt.Printf("a: %v, s1: %v, s2: %v\n", a, s1, s2)

s3 := a[:2:2]
s3 = append(s3, 101)

fmt.Printf("a: %v, s1: %v, s2: %v, s3:%v\n", a, s1, s2, s3)
}

输出结果

a: [1 2 3 4], s1: [3 4]
a: [1 2 3 4], s2: [1 2]
a: [1 2 3 4], s1: [3 4], s2: [1 2 100]
a: [1 2 3 4], s1: [3 4], s2: [1 2 100], s3:[1 2 101]

从上面的例子可以看到,限定了切片的容量之后,从同一数据产生的切片的

slice 的基本操作和数组一致
slice 不可以用 == 比较的, bytes 提供了 []byte 的比较方法, strings 提供了 []string 的比较方法

map

一个无序的K/V集合,其中所有的key 都是唯一的,在 gomap 中的 key 必须是可比较的(支持 == 比较运算符)的数据类型, 所以 key 不能是 mapslice 或者 func, 而对于value 值没有任何要求

map 的定义和初始化

1
2
var mapDemo map[Key]Value
m := make(map[Key]Value, len)

mapkey 是完全无序的, 使用 for 循环遍历map 的时候每次的顺序都是随机的

判断 map m1 中是否包含 k1

1
2
3
4
5
var m1 = make(map[Key][Value])
val1, contains := m1[k1]
if contains {
fmt.Println("the map m1 contains k1!")
}

注意:对于 slice 可以直接往为 nilslice 中存放数据,但是map 必须先使用 make 分配内存

结构体(struct)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个结构体
type Student struct {
Name string
id int64
}

// 定义结构体的 String 函数
func (s Student) String() string {
return fmt.Sprintf("name: %v, Id: %v", s.Name, s.id)
}

// 实例化一个结构体
var s Student
// 访问结构体的数据
s.Name = "test"

结构体还可以组合, 下面我们尝试定义两个结构体,CircleRectangle

1
2
3
4
5
6
7
type Circle struct {
x, y, r int64
}

type Rectangle struct {
x, y, len, width int64 // x,y 是左下角的点
}

可以看到 RectangleCircle 都具有点的属性,可以提取出一个结构体 Point
下面的代码中我们定义了三个结构体,分别是 PointCircleRectangle, 其中CircleRectangle 中都组合了一个 Point, 这是访问属性的时候就需要先访问被组合的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Point struct {
x, y int64
}

type Circle struct {
c Point // 原点
r int64 // 半径
}

// 矩形
type Rectangle struct {
p Point // 左下角的点
len, width int64 // 长和宽
}

func main() {
c1 := Circle{c: Point{x: 0, y: 0}, r: 2}
fmt.Println(c1)

c2 := Circle{}
c2.c.x = 0
c2.c.y = 0
c2.r = 3
fmt.Println(c2)

r1 := Rectangle{p: Point{x:0, y:0}, len:4, width:3}
fmt.Println(r1)
}

go还支持在结构体中只声明属性的数据类型,而不只限定属性的名称, 这就是匿名成员,从下面的 代码中也能看到,匿名成员也不是没有名字,而是直接把数据类型作为了成员的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Point struct {
x int64
y int64
}

type Circle struct {
Point // 原点
r int64 // 半径
}

// 矩形
type Rectangle struct {
Point // 左下角的点
len, width int64 // 长和宽
}

func main() {
c1 := Circle{Point: Point{x: 0, y: 0}, r: 2}
fmt.Println(c1)

c2 := Circle{}
c2.Point.x = 0
c2.Point.y = 0
c2.r = 3
fmt.Println(c2)

r1 := Rectangle{Point: Point{x: 0, y: 0}, len: 4, width: 3}
fmt.Println(r1)
}

结构体是多个类型数据聚合的数据类型, 可以包含任何类型的属性,结构体的访问权限通过大小写来控制,只有大写开头的属性和结构体具有包外可见性,使用json序列化和反序列化数组的时候,小写开头的属性会被忽略。

结构体的比较取决于结构体的属性,当结构体的所有属性都是可比较的时候,结构体就是可比较的,当使用 == 比较两个结构体的示例的时候,这就是。针对包含不可比较的结构体,也可以使用反射reflect.DeepEqual()

函数

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

1
2
3
func funcName(param1 Type, param2 Type) (result1 Type, result2 Type) {
// method body
}

从上面的例子中,我们可以看到go 函数的一个显著特点:多返回值
有时候一个方法的结果除了正常的结果外,还有不可预期的异常,这个时候就可以返回多个返回值,比如 encode/json 中的序列化的方法,正常情况下返回序列化的结果,而如果输入的参数无法序列化的时候则返回nil 和异常

1
2
3
4
5
6
7
8
func Marshal(v interface{}) ([]byte, error) {
e := &encodeState{}
err := e.marshal(v, encOpts{escapeHTML: true})
if err != nil {
return nil, err
}
return e.Bytes(), nil
}

如果我们在返回值列表中写明变量名,就可以在return 语句中省略操作数, 上面的方法就可以改写成下面这种

1
2
3
4
5
6
7
8
9
func Marshal(v interface{}) (res []byte, err error) {
e := &encodeState{}
err = e.marshal(v, encOpts{escapeHTML: true})
if err != nil {
return // 等同于 return nil, err
}
res = e.Bytes()
return // 等同于 return e.Bytes(), nil
}
method is first-class value

在 go 语言中函数也是 first-class valuefirst-class value 的定义如下:

  • 可以作为变量或者数据结构存储
  • 可以作为参数传递给方法/函数
  • 可以作为返回值从函数/方法返回
  • 可以在运行期创建
  • 有固有身份;“固有身份”是指实体有内部表示,而不是根据名字来识别,比如匿名函数,还可以通过赋值叫任何名字。大部分语言的基本类型的数值(int, float)等都是第一类对象;但是数组不一定,比如C中的数组,作为函数参数时,传递的是第一个元素的地址,同时还丢失了数组长度信息。对于大多数的动态语言,函数/方法都是第一类对象,比如Python,但是Ruby不是,因为不能返回一个方法。第一类函数对函数式编程语言来说是必须的。

也就是说在 go语言中函数拥有类型,可以被赋值,可以作为函数的参数或返回值,还可以有匿名函数

可变参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func sum(args ...int) int {
total := 0
for _, a := range args {
total += a
}
return total
}

fmt.Println(sum()) // 0
fmt.Println(sum(1)) // 1
fmt.Println(sum(1,2,3,4)) // 10

arr := []int{1,2,3,4}
fmt.Println(sum(arr...)) // 10
defer

有时候我们需要在函数执行结束之前释放资源(如数据库链接)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func parseFromFile(fileName string) (res []string, err error) {
inputFile, err := os.Open(fileName)
if err != nil {
fmt.Printf("An error occurred on opening the inputfile\n" +
"Does the file exist?\n" +
"Have you got acces to it?\n")
return
}

inputReader := bufio.NewReader(inputFile)
for {
inputString, err := inputReader.ReadString('\n')
fmt.Printf("The input was: %s", inputString)
if err != nil {
inputFile.Close()
return
}
if (err = checkLine(inputString)); err != nil {
inputFile.Close()
return
}
res = append(res, inputString)
}
}

可以看到在每个 return 语句之前,我们都需要调用 inputFile.Close(), 这样做是在太麻烦,go 为我们提供了 refer 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func parseFromFile(fileName string) (res []string, err error) {
inputFile, err := os.Open(fileName)
if err != nil {
fmt.Printf("An error occurred on opening the inputfile\n" +
"Does the file exist?\n" +
"Have you got acces to it?\n")
return
}
refer inputFile.Close() // 在函数执行到return 之前被调用

inputReader := bufio.NewReader(inputFile)
for {
inputString, err := inputReader.ReadString('\n')
fmt.Printf("The input was: %s", inputString)
if err != nil {
return
}
if (err = checkLine(inputString)); err != nil {
return
}
res = append(res, inputString)
}
}
refer 执行顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func func1() {
log.Printf("this is func 1\n")
}

func func2() {
log.Printf("this is func 2\n")
}

func func3() {
log.Printf("this is func 3\n")
}

func func4() {
log.Printf("this is func 4\n")
}

func main() {
log.Printf("begin\n")
defer func1()
defer func2()
defer func3()
defer func4()
log.Printf("finished\n")
}

上面程序的数据输出结果是:

2018/04/17 08:11:41 begin
2018/04/17 08:11:41 finished
2018/04/17 08:11:41 this is func 4
2018/04/17 08:11:41 this is func 3
2018/04/17 08:11:41 this is func 2
2018/04/17 08:11:41 this is func 1

可以看出来:defer 的执行顺序和定义顺序正好是相反的

循环中的 defer
1
2
3
4
5
6
7
8
9
10
11
// before process file
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // NOTE: risky; could run out of file
// ...process f
}
// after process file
return

在上面的程序中我们遍历文件名称数组,一个个的处理文件,并在处理完之后希望去关闭文件; 但是 defer 是在函数执行完最后一步才触发,如果文件比较多可能会耗光系统的文件描述符

这里的解决方案是把 for 循环的循环体抽取成函数

1
2
3
4
5
6
7
8
9
10
11
12
13
for _, filename := range filenames {
if err := doFile(filename); err != nil {
return err
}
}
func doFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// ...process f…
}
init 函数
1
2
3
func init() {
// init something
}

方法

前面讲结构体的时候, 讲过可以给结构体定义方法, 事实上除了结构体我们还可以们可以给数值、字符串、map、数组定义一些自定义行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//自定义int 数组
type array []int

//给数组定义 sum 方法
func (a array) sum() int {
total := 0
for _, i := range a {
total += i
}
return total
}

func main() {
var arr1 = array{1, 2, 3, 4}
arrSum := arr1.sum()
fmt.Println(arrSum) // 输出 10

arrSum2 := array.sum(arr1) // 常规调动函数的方法
fmt.Println(arrSum2) // 输出 10
}

接口

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

  1. 内嵌结构体的函数
  2. 垃圾回收
  3. 协程池
  4. 并发
  5. etcd