go语言基础

1. 初识

1.1 特性

  • 自动垃圾回收——GC
  • 更丰富的内置类型——map、slice
  • 函数的多返回值——python
  • 错误处理——defer、panic、recover
  • 匿名函数和闭包
  • 类型和接口
  • 并发编程——goruntine
  • 反射
  • 语言交互性

1.2 命名规范

1.2.1 区分大小写的语言

命名规则涉及变量、常量、全局函数、结构、接口、方法等的命名。 Go语言从语法层面进行了以下限定:任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。

  1. 当命名(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Analysize,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);
  2. 命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )
1.2.2 包名称

保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为小写单词,不要使用下划线或者混合大小写。

package domain
package main
1.2.3 文件命名

尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词。

approve_service.go
1.2.4 结构体命名
  • 采用驼峰命名法,首字母根据访问控制大写或者小写

  • struct 申明和初始化格式采用多行,例如下面:

    type MainConfig struct {
        Port string `json:"port"`
        Address string `json:"address"`
    }
    config := MainConfig{"1234", "123.221.134"}
    
1.2.5 接口命名
  • 命名规则基本和上面的结构体类型

  • 单个函数的结构名以 “er” 作为后缀,例如 Reader , Writer 。

    type Reader interface {
            Read(p []byte) (n int, err error)
    }
    
1.2.6 变量命名

和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:

  • 如果变量为私有,且特有名词为首个单词,则使用小写,如 appService
  • 若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
1.2.7 常量命名

常量均需使用全部大写字母组成,并使用下划线分词

const APP_URL = "https://www.baidu.com"

如果是枚举类型的常量,需要先创建相应类型:

type Scheme string

const (
    HTTP  Scheme = "http"
    HTTPS Scheme = "https"
)

2. 顺序编程

2.1 类型

类型都是值传递的,改变变量的值,只能传递指针

类型含义
数组数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。
切片Slice对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者Python中的 list 类型)
映射map一种特殊的数据结构,一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),其他编程语言中也称为字典(Python)、hash 和 HashTable
列表list列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等
2.1.1 数组
var 数组变量名 [元素数量]Type

遍历数组

var a [3]int             // 定义三个整数的数组
fmt.Println(a[0])        // 打印第一个元素
fmt.Println(a[len(a)-1]) // 打印最后一个元素
// 打印索引和元素
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}
// 仅打印元素
for _, v := range a {
    fmt.Printf("%d\n", v)
}

2.1.2 切片
slice [开始位置 : 结束位置]

// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)

使用 make() 函数构造切片

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

make( []Type, size, cap)

其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。

示例如下:

a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))

代码输出如下:

[0 0] [0 0]

2 2

append()为切片添加元素

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包

切片复制

copy( destSlice, srcSlice []T) int

//copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制
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语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。

从开头位置删除

删除开头的元素可以直接移动数据指针:

a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):

a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素

还可以用 copy() 函数来删除开头的元素:

a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素

删除指定位置的元素

package main
import "fmt"
func main() {
    seq := []string{"a", "b", "c", "d", "e"}
    // 指定删除位置
    index := 2
    // 查看删除位置之前的元素和之后的元素
    fmt.Println(seq[:index], seq[index+1:])
    // 将删除点前后的元素连接起来
    seq = append(seq[:index], seq[index+1:]...)
    fmt.Println(seq)
}

2.1.3 映射
var mapname map[keytype]valuetype

package main
import "fmt"
func main() {
    var mapLit map[string]int
    //var mapCreated map[string]float32
    var mapAssigned map[string]int
    mapLit = map[string]int{"one": 1, "two": 2}
    mapCreated := make(map[string]float32)
    mapAssigned = mapLit
    mapCreated["key1"] = 4.5
    mapCreated["key2"] = 3.14159
    mapAssigned["two"] = 3
    fmt.Printf("Map literal at \"one\" is: %d\n", mapLit["one"])
    fmt.Printf("Map created at \"key2\" is: %f\n", mapCreated["key2"])
    fmt.Printf("Map assigned at \"two\" is: %d\n", mapLit["two"])
    fmt.Printf("Map literal at \"ten\" is: %d\n", mapLit["ten"])
    

map 容量

和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:

make(map[keytype]valuetype, cap)

例如:

map2 := make(map[string]float, 100)

当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

这里有一个 map 的具体例子,即将音阶和对应的音频映射起来:

noteFrequency := map[string]float32 {"C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,"G0": 24.50, "A0": 27.50, "B0": 30.87, "A4": 440}

2.1.4 列表

初始化

  1. 通过 container/list 包的 New() 函数初始化 list

变量名 := list.New()

  1. 通过 var 关键字声明初始化 list

var 变量名 list.List

插入元素

方 法功 能
InsertAfter(v interface {}, mark * Element) * Element在 mark 点之后插入元素,mark 点由其他插入函数提供
InsertBefore(v interface {}, mark * Element) *Element在 mark 点之前插入元素,mark 点由其他插入函数提供
PushBackList(other *List)添加 other 列表元素到尾部
PushFrontList(other *List)添加 other 列表元素到头部

删除元素

package main
import "container/list"
func main() {
    l := list.New()
    // 尾部添加
    l.PushBack("canon")
    // 头部添加
    l.PushFront(67)
    // 尾部添加后保存元素句柄
    element := l.PushBack("fist")
    // 在fist之后添加high
    l.InsertAfter("high", element)
    // 在fist之前添加noon
    l.InsertBefore("noon", element)
    // 使用
    l.Remove(element)
}

每次操作列表变化

操作内容列表元素
l.PushBack(“canon”)canon
l.PushFront(67)67, canon
element := l.PushBack(“fist”)67, canon, fist
l.InsertAfter(“high”, element)67, canon, fist, high
l.InsertBefore(“noon”, element)67, canon, noon, fist, high
l.Remove(element)67, canon, noon, high

遍历列表

遍历双链表需要配合 Front() 函数获取头元素,遍历时只要元素不为空就可以继续进行,每一次遍历都会调用元素的 Next() 函数

l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
for i := l.Front(); i != nil; i = i.Next() {
    fmt.Println(i.Value)
}

在Go语言中,布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串"",而指针、切片、映射、通道、函数和接口的零值则是 nil

2.2 流程控制

2.2.1 分支结构

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

if condition1 {
    // do something
} else if condition2 {
    // do something else
}else {
    // catch-all or default
}

if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,代码如下:

if err := Connect(); err != nil {    
    fmt.Println(err)    
    return
}

Connect 是一个带有返回值的函数,err:=Connect() 是一个语句,执行 Connect 后,将错误保存到 err 变量中。

err != nil 才是 if 的判断表达式,当 err 不为空时,打印错误并返回。

2.2.2 循环结构

与多数语言不同的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构,关键字 for 的基本使用方法与C语言和 C++ 中非常接近:

sum := 0
for i := 0; i < 10; i++ {    
    sum += i
}

无限循环

sum := 0
for {
    sum++
    if sum > 100 {
        break
    }
}

for range 结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道(channel)

通过 for range 遍历的返回值有一定的规律:

  • 数组、切片、字符串返回索引和值。
  • map 返回键和值。
  • 通道(channel)只返回通道内的值。
2.2.3 选择结构

Go语言改进了 switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行,示例代码如下:

var a = "hello"
switch a {
case "hello":    
	fmt.Println(1)
case "world":    
	fmt.Println(2)
default:    
	fmt.Println(0)}

1) 一分支多值

当出现多个 case 要放在一起的时候,可以写成下面这样:

var a = "mum"
switch a {
    case "mum", "daddy":    
    	fmt.Println("family")
}

不同的 case 表达式使用逗号分隔。

2) 分支表达式

case 后不仅仅只是常量,还可以和 if 一样添加表达式,代码如下:

var r int = 11
switch {
    case r > 10 && r < 20:    
    fmt.Println(r)
}

注意,这种情况的 switch 后面不再需要跟判断变量。

2.2.4 关键字
名称用途
goto通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助
break结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的 for、switch 和 select 的代码块上
continue结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加标签时,表示开始标签对应的循环

goto退出多重循环

package main
import "fmt"
func main() {
    for x := 0; x < 10; x++ {
        for y := 0; y < 10; y++ {
            if y == 2 {
                // 跳转到标签
                goto breakHere
            }
        }
    }
    // 手动返回, 避免执行进入标签
    return
    // 标签
breakHere:
    fmt.Println("done")
}

代码说明如下:

  • 第 1 行,执行某逻辑,返回错误。
  • 第 2~6 行,如果发生错误,打印错误退出进程。
  • 第 8 行,执行某逻辑,返回错误。
  • 第 10~14 行,发生错误后退出流程。
  • 第 16 行,没有任何错误,打印完成。

goto集中错误处理

 err := firstCheckError()
    if err != nil {
        goto onExit
    }
    err = secondCheckError()
    if err != nil {
        goto onExit
    }
    fmt.Println("done")
    return
onExit:
    fmt.Println(err)
    exitProcess()

代码说明如下:

  • 第 3 行和第 9 行,发生错误时,跳转错误标签 onExit。
  • 第 17 行和第 18 行,汇总所有流程进行错误打印并退出进程

break跳出循环

package main
import "fmt"
func main() {
OuterLoop:
    for i := 0; i < 2; i++ {
        for j := 0; j < 5; j++ {
            switch j {
            case 2:
                fmt.Println(i, j)
                break OuterLoop
            case 3:
                fmt.Println(i, j)
                break OuterLoop
            }
        }
    }
}

continue结束内存循环开启外层循环

package main
import "fmt"
func main() {
OuterLoop:
    for i := 0; i < 2; i++ {
        for j := 0; j < 5; j++ {
            switch j {
            case 2:
                fmt.Println(i, j)
                continue OuterLoop
            }
        }
    }
}

2.3 函数

Go语言里面拥三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者 lambda 函数
  • 方法
2.3.1 普通函数声明

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

func 函数名(形式参数列表)(返回值列表){
    函数体
}

func hypot(x, y float64) float64 {
	return math.Sqrt(x*x + y*y)
}

2.3.2 返回值

1)同一类型返回值

返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型

func typedTwoValues() (int, int) {
    return 1, 2
}
func main() {
    a, b := typedTwoValues()
    fmt.Println(a, b)
}

2) 带有变量名的返回值

Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型

func namedRetValues() (a, b int) {
    a = 1
    b = 2
    return
}

调用函数

返回值变量列表 = 函数名(参数列表)
result := add(1,1)

2.3.4 函数变量

在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f,并将一个函数名为 fire() 的函数赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数

package main
import (
    "fmt"
)
func fire() {
    fmt.Println("fire")
}
func main() {
    var f func()
    f = fire
    f()
}

2.3.5 匿名函数

不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成

func(参数列表)(返回参数列表){
    函数体
}

1) 在定义时调用匿名函数

匿名函数可以在声明后调用,例如:

func(data int) {    
fmt.Println("hello", data)
}(100)

注意第3行

}

后的(100),表示对匿名函数进行调用,传递参数为 100。

2) 将匿名函数赋值给变量

匿名函数可以被赋值,例如:

// 将匿名函数体保存到f()中
f := func(data int) {    
fmt.Println("hello", data)
}
// 使用f()调用
f(100)

匿名函数作回调函数

package main
import (
    "fmt"
)
// 遍历切片的每个元素, 通过给定函数进行元素访问
func visit(list []int, f func(int)) {
    for _, v := range list {
        f(v)
    }
}
func main() {
    // 使用匿名函数打印切片内容
    visit([]int{1, 2, 3, 4}, func(v int) {
        fmt.Println(v)
    })
}

代码说明如下:

  • 第 8 行,使用 visit() 函数将整个遍历过程进行封装,当要获取遍历期间的切片值时,只需要给 visit() 传入一个回调参数即可。
  • 第 18 行,准备一个整型切片 []int{1,2,3,4} 传入 visit() 函数作为遍历的数据。
  • 第 19~20 行,定义了一个匿名函数,作用是将遍历的每个值打印出来。

使用匿名函数实现操作封装

package main
import (
    "flag"
    "fmt"
)
var skillParam = flag.String("skill", "", "skill to perform")
func main() {
    flag.Parse()
    var skill = map[string]func(){
        "fire": func() {
            fmt.Println("chicken fire")
        },
        "run": func() {
            fmt.Println("soldier run")
        },
        "fly": func() {
            fmt.Println("angel fly")
        },
    }
    if f, ok := skill[*skillParam]; ok {
        f()
    } else {
        fmt.Println("skill not found")
    }
}

代码说明如下:

  • 第 8 行,定义命令行参数 skill,从命令行输入 --skill 可以将=后的字符串传入 skillParam 指针变量。
  • 第 12 行,解析命令行参数,解析完成后,skillParam 指针变量将指向命令行传入的值。
  • 第 14 行,定义一个从字符串映射到 func() 的 map,然后填充这个 map。
  • 第 15~23 行,初始化 map 的键值对,值为匿名函数。
  • 第 26 行,skillParam 是一个 *string 类型的指针变量,使用 *skillParam 获取到命令行传过来的值,并在 map 中查找对应命令行参数指定的字符串的函数。
  • 第 29 行,如果在 map 定义中存在这个参数就调用,否则打印“技能没有找到”。
2.3.6 闭包

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:

函数 + 引用环境 = 闭包

闭包的记忆效应

package main
import (
	"fmt"
)
// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
	// 返回一个闭包
	return func() int {
		// 累加
		value++
		// 返回一个累加值
		return value
	}
}
func main() {
	// 创建一个累加器, 初始值为1
	accumulator := Accumulate(1)
	// 累加1并打印
	fmt.Println(accumulator())
	fmt.Println(accumulator())
	// 打印累加器的函数地址
	fmt.Printf("%p\n", &accumulator)
	// 创建一个累加器, 初始值为10
	accumulator2 := Accumulate(10)
	// 累加1并打印
	fmt.Println(accumulator2())
	// 打印累加器的函数地址
	fmt.Printf("%p\n", &accumulator2)
}

代码说明如下:

  • 第 8 行,累加器生成函数,这个函数输出一个初始值,调用时返回一个为初始值创建的闭包函数。
  • 第 11 行,返回一个闭包函数,每次返回会创建一个新的函数实例。
  • 第 14 行,对引用的 Accumulate 参数变量进行累加,注意 value 不是第 11 行匿名函数定义的,但是被这个匿名函数引用,所以形成闭包。
  • 第 17 行,将修改后的值通过闭包的返回值返回。
  • 第 24 行,创建一个累加器,初始值为 1,返回的 accumulator 是类型为 func()int 的函数变量。
  • 第 27 行,调用 accumulator() 时,代码从 11 行开始执行匿名函数逻辑,直到第 17 行返回。
  • 第 32 行,打印累加器的函数地址。

闭包实现生成器

闭包的记忆效应被用于实现类似于设计模式中工厂模式的生成器,下面的例子展示了创建一个玩家生成器的过程

package main

import (
    "fmt"
)

// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {

    // 血量一直为150
    hp := 150

    // 返回创建的闭包
    return func() (string, int) {

        // 将变量引用到闭包中
        return name, hp
    }
}

func main() {

    // 创建一个玩家生成器
    generator := playerGen("high noon")

    // 返回玩家的名字和血量
    name, hp := generator()

    // 打印值
    fmt.Println(name, hp)
}

2.3.7 可变参数

形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数,它是一个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用,通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的可能

可变参数类型

func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

myfunc(2, 3, 4)

任意类型的可变参数

用 interface{} 传递任意类型数据是Go语言的惯例用法

func Printf(format string, args ...interface{}) {
    // ...
}

package main
import "fmt"
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.")
        }
    }
}
func main() {
    var v1 int = 1
    var v2 int64 = 234
    var v3 string = "hello"
    var v4 float32 = 1.234
    MyPrintf(v1, v2, v3, v4)
}


遍历可变参数列表——获取每一个参数的值

package main
import (
    "bytes"
    "fmt"
)
// 定义一个函数, 参数数量为0~n, 类型约束为字符串
func joinStrings(slist ...string) string {
    // 定义一个字节缓冲, 快速地连接字符串
    var b bytes.Buffer
    // 遍历可变参数列表slist, 类型为[]string
    for _, s := range slist {
        // 将遍历出的字符串连续写入字节数组
        b.WriteString(s)
    }
    // 将连接好的字节数组转换为字符串并输出
    return b.String()
}
func main() {
    // 输入3个字符串, 将它们连成一个字符串
    fmt.Println(joinStrings("pig ", "and", " rat"))
    fmt.Println(joinStrings("hammer", " mom", " and", " hawk"))
}

2.3.8 延迟执行

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件

多个延迟执行语句的处理顺序

package main
import (
    "fmt"
)
func main() {
    fmt.Println("defer begin")
    // 将defer放入延迟调用栈
    defer fmt.Println(1)
    defer fmt.Println(2)
    // 最后一个放入, 位于栈顶, 最先调用
    defer fmt.Println(3)
    fmt.Println("defer end")
}

代码输出如下:
defer begin
defer end
3
2
1

1) 使用延迟并发解锁

在下面的例子中会在函数中并发使用 map,为防止竞态问题,使用 sync.Mutex 进行加锁

func readValue(key string) int {
    valueByKeyGuard.Lock()
   
    // defer后面的语句不会马上调用, 而是延迟到函数结束时调用
    defer valueByKeyGuard.Unlock()
    return valueByKey[key]
}

2) 使用延迟释放文件句柄

文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源,在下面的例子中将实现根据文件名获取文件大小的函数,函数中需要打开文件、获取文件大小和关闭文件等操作,由于每一步系统操作都需要进行错误处理,而每一步处理都会造成一次可能的退出,因此就需要在退出时释放资源,而我们需要密切关注在函数退出处正确地释放文件资源

func fileSize(filename string) int64 {
    f, err := os.Open(filename)
    if err != nil {
        return 0
    }
    // 延迟调用Close, 此时Close不会被调用
    defer f.Close()
    info, err := f.Stat()
    if err != nil {
        // defer机制触发, 调用Close关闭文件
        return 0
    }
    size := info.Size()
    // defer机制触发, 调用Close关闭文件
    return size
}

2.3.9 递归函数

递归函数指的是在函数内部调用函数自身的函数,从数学解题思路来说,递归就是把一个大问题拆分成多个小问题,再各个击破,在实际开发过程中,递归函数可以解决许多数学问题,如计算给定数字阶乘、产生斐波系列等.

构成递归需要具备以下条件:

  • 一个问题可以被拆分成多个子问题;
  • 拆分前的原问题与拆分后的子问题除了数据规模不同,但处理问题的思路是一样的;
  • 不能无限制的调用本身,子问题需要有退出递归状态的条件

斐波那契数列

package main

import "fmt"

func main(){
	result := 0
	for i := 1; i <= 10; i++ {
		result = fibonacci(i)
		fmt.Printf("fibonacci(%d) is %d", i, result)
	}
}

func fibonacci(n int) (res int) {
	if n <= 2 {
		res = 1
	}else{
		res = fibonacci(n-1) + fibonacci(n-2)
	}
	return res
}

数字阶乘

package main

import "fmt"

func main(){
    result := 0
    for i := 1; i <= 10; i++ {
        result = factorial(i)
        fmt.Printf("factorial(%d) is %d", i, result)
    }
}

func factorial(n int) (res int){
    if n > 0 {
        // n!=n*(n-1)!
        res = n * factorial(n-1)
        return res
    }
    return 1
}

3. 面向对象编程

3.1 类型系统

类型都是值传递的,改变变量的值,只能传递指针

3.1.1 值语义和引用语义

值语义和引用语义的差别在于赋值

b = a
b.Modify()

如果b的修改不会影响a的值,那么此类型属于值类型,否则为引用类型

大多数类型都属于值语义,包括:

  • 基本类型:byte、int、bool、float32、float64和string
  • 复合类型:array、struct、pointer等

引用语义,包括:

  • slice:切片
  • map:映射
  • channel :执行体(goruntime)间的通信设施
  • interface:接口
3.1.2 结构体

结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。

结构体成员也可以称为“字段”,这些字段有以下特性:

  • 字段拥有自己的类型和值;
  • 字段名必须唯一;
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

使用关键字 type可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。

结构体的定义格式如下:

type 类型名 struct {
    字段1 字段1类型
    字段2 字段2类型
    …
}

对各个部分的说明:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • struct{}:表示结构体类型,type 类型名 struct{}可以理解为将 struct{} 结构体定义为类型名的类型。
  • 字段1、字段2……:表示结构体字段名,结构体中的字段名必须唯一。
  • 字段1类型、字段2类型……:表示结构体各个字段的类型。

实例化结构体

1、实例化–var声明

type Point struct {
	X int
	Y int
}

var p Point
p.X = 10
p.Y = 20

2、实例化–new关键字

Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体

使用 new 的格式如下:

//ins := new(T)
//其中:
//T 为类型,可以是结构体、整型、字符串等。
//ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。

type Player struct{
    Name string
    HealthPoint int
    MagicPoint int
}
tank := new(Player)
tank.Name = "Canon"
tank.HealthPoint = 300

Go语言和 C/C++

在 C/C++ 语言中,使用 new 实例化类型后,访问其成员变量时必须使用->操作符。

在Go语言中,访问结构体指针的成员变量时可以继续使用‘.’,这是因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将 ins.Name 形式转换为 (*ins).Name。

取结构体的地址实例化

在go语言中,对结构体进行&取地址操作时,视为对该类型进行一次new的实例化操作,去地址格式如下:

ins := &T{}

其中:

  • T表示结构体类型
  • ins为结构体的实例,类型为*T,是指针类型
type Command struct {
    Name    string    // 指令名称
    Var     *int      // 指令绑定的变量
    Comment string    // 指令的注释
}
var version int = 1
cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"

fmt.Printf("cmd point: %p\n", cmd)

cmd1 := &Command{}
cmd1.Name = "version"
cmd1.Var = &version
cmd1.Comment = "show version"
fmt.Printf("cmd1 point: %p\n", cmd1)

取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程

func newCommand(name string, varref *int, comment string) *Command {
    return &Command{
        Name:    name,
        Var:     varref,
        Comment: comment,
    }
}
cmd = newCommand(
    "version",
    &version,
    "show version",
)

3、赋值初始化

func main()  {
	fan := person{
		name: "fan",
		age:  10,
	}
}

//赋值顺序
func main()  {
	fan := person{
		"fan",
		 10,
	}
}


初始化结构体

结构体在实例化时可以直接对成员变量进行初始化,初始化有两种形式分别是以字段“键值对”形式和多个值的列表形式,键值对形式的初始化适合选择性填充字段较多的结构体,多个值的列表形式适合填充字段较少的结构体。

//键值对填充结构体
type People struct {
    name  string
    child *People
}
relation := &People{
    name: "爷爷",
    child: &People{
        name: "爸爸",
        child: &People{
                name: "我",
        },
    },
}
//多值列表初始化结构体
type Address struct {
    Province    string
    City        string
    ZipCode     int
    PhoneNumber string
}
addr := Address{
    "四川",
    "成都",
    610000,
    "0",
}
fmt.Println(addr)

3.1.3 可见性

使用大写字母开头的符号,能对其他包可见

3.1.4 接口
package main
import (
    "fmt"
)
// 定义一个Invoker接口
type Invoker interface {
    // 需要实现一个Call方法
    Call(interface{})
}
// 结构体类型
type Struct struct {
}
// 实现Invoker的Call
func (s *Struct) Call(p interface{}) {
    fmt.Println("from struct", p)
}
// 函数定义为类型
type FuncCaller func(interface{})
// 实现Invoker的Call
func (f FuncCaller) Call(p interface{}) {
    // 调用f函数本体
    f(p)
}
func main() {
    // 声明接口变量
    var invoker Invoker
    // 实例化结构体
    s := new(Struct)
    // 将实例化的结构体赋值到接口
    invoker = s
    // 使用接口调用实例化结构体的方法Struct.Call
    invoker.Call("hello")
    // 将匿名函数转为FuncCaller类型,再赋值给接口
    invoker = FuncCaller(func(v interface{}) {
        fmt.Println("from function", v)
    })
    // 使用接口调用FuncCaller.Call,内部会调用函数本体
    invoker.Call("hello")
}

package main

import "fmt"
import "math"


type Shape interface {
	area() float64

}

type MultiShape interface {
	Shape            //嵌入式
}

type Rectangle struct {
	width float64
	height float64
}

type Circle struct {
	radius float64
}

func (r Rectangle) area() float64 {
	return r.height * r.width
}

func (c Circle) area() float64 {
	return math.Pi * math.Pow(c.radius,2)
}

func getArea(shape MultiShape) float64 {        //改为MultiShape
	return shape.area()
}

func main(){
	r := Rectangle{20,10}
	c := Circle{4}
	fmt.Println("Rectangle Area =",getArea(r))
	fmt.Println("Circle Area =",getArea(c))

}


4. 并发编程

4.1 并发基础

  • 多进程。多进程是操作系统层面进行并发的基本模式,也是开销最大的模式。好处在于简单、进程间互不影响,坏处在于由内核管理,系统开销大;
  • 多线程。多线程是系统层面的并发模式。比多进程开销小很多,但依旧较大,且在高并发模式下,效率会有影响。
  • 基于回调的非阻塞/异步IO。源于多线程会很快耗尽内存和CPU资源。通过事件驱动的方式使用异步IO,尽可能地少使用线程,降低开销。
  • 协程。用户态线程。轻量级线程,系统开销小,编程简单,结构清晰,支持语言少

4.2 goruntime

Go语言中协程(轻量级线程)实现,使用关键字go

4.3 channel

“不要通过共享内存来通信,而应该通过通信来共享内存”

package main

import "fmt"

func Count(ch chan int) {
	fmt.Println("Counting")
	ch <- 1
}

func main() {
	chs := make([]chan int, 10)
	for i := 0;i < 10;i++{
		chs[i] = make(chan int)
		go Count(chs[i])
	}
	for _, ch := range chs {
		<- ch
	}
}

4.3.1 基本语法

一般声明形式

var chanName chan ElementType

声明并初始化

ch := make(chan int)

常见的用法,写入和读取

// 写入,会导致程序阻塞,直到有其他goruntime从这个channel中读取数据
ch <- value
// 读取,如果之前没有写入数据,会导致程序阻塞,直到被写入数据为止
value := <- ch

4.3.2 select

select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述,每个case语句里必须有个channel操作,用于 处理异步IO问题

select {
    case <-chan1:
    // 如果chan1成功读到数据,则进行
    case chan2 <- 1:
    // 如果成功向chan2写入数据,则进行
    default:
    // 如果上面都没成功,则进行
}

4.3.3 缓冲机制
c := make(chan int, 1024)

即使没有读取方,写入方也可以一直往channel里写入,在缓冲区填完之前都不会阻塞

4.3.4 超时机制

Go语言没有提供直接的超时处理机制,但可以利用select机制

// 实现并执行一个匿名超时等待函数
timeout := make(chan bool, 1)
go func() {
    time.Sleep(1e9) //等待1秒钟
    timeout <- true
}()

select {
    case <- ch:
    //从ch中读取数据
    case <- timeout:
    // 一直没有从ch中读取数据,但从timeout中读取到了数据
}

程序会在timeout中获取到一个数据后继续执行,无论对ch的读取是否还处于等待状态,从而达到1秒超时效果

4.4 同步

4.4.1 同步锁

sync包中提供了两种锁类型:sync.Mutex和sync.RWMutex。Mutex是最简单的一种锁类型,当一个goroutine获得Mutex后,其他goruntine就只能等待这个goruntine释放该Mutex。RWMutex相对友好,经典的单写多读模型,在读锁占用的情况下,只会阻止写,也就是多个goruntine可同时获取读锁(调用RLock()方法;而写锁(调用Lock()方法,会阻止任何其他goruntine进来,相当于独占))

//一一对应,否则导致该锁的所有goruntine处于饥饿状态,甚至死锁
// 写锁
Lock() <--> Unlock()   
// 读锁
RLock() <--> RUnlock()

4.4.2 全局唯一性

全局初始化操作

var a string
var once sync.Once

func setup() {
    a = "hello, world"
}

func doprint() {
    once.Do(setup)
    print(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

once的Do方法保证全局访问内只调用制定的函数一次

测试

Go语言自带了 testing 测试包,可以进行自动化的单元测试,输出结果验证,并且可以测试性能

测试规则

要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀,例如:

func TestXxx( t *testing.T ){
    //......
}

编写测试用例有以下几点需要注意:

  • 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;
  • 测试用例的文件名必须以_test.go结尾;
  • 需要使用 import 导入 testing 包;
  • 测试函数的名称要以TestBenchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数;
  • 单元测试则以(t *testing.T)作为参数,性能测试以(t *testing.B)做为参数;
  • 测试用例文件使用go test命令来执行,源码中不需要 main() 函数作为入口,所有以_test.go结尾的源码文件内以Test开头的函数都会自动执行。

Go语言的 testing 包提供了三种测试方式,分别是单元(功能)测试、性能(压力)测试和覆盖率测试。

测试模块

package demo
// 根据长宽获取面积
func GetArea(weight int, height int) int {
    if weight > height{
		return weight * height
	}
    return weight * height
}

单元(功能)测试

package demo
import "testing"
func TestGetArea(t *testing.T) {
    area := GetArea(40, 50)
    if area != 2000 {
        t.Error("测试失败")
    }
}

执行命令:

go test -v
=== RUN   TestGetArea
--- PASS: TestGetArea (0.00s)
PASS
ok      _/home/hjy/TestCode/go/demo     0.001s


性能(压力)测试

package demo
import "testing"
func BenchmarkGetArea(t *testing.B) {
    for i := 0; i < t.N; i++ {
        GetArea(40, 50)
    }
}

执行命令:

go test -bench='.'

goos: linux
goarch: amd64
BenchmarkGetArea-6      1000000000               0.256 ns/op
PASS
ok      _/home/hjy/TestCode/go/demo     0.285s


覆盖率测试

覆盖率测试能知道测试程序总共覆盖了多少业务代码(也就是 demo_test.go 中测试了多少 demo.go 中的代码),可以的话最好是覆盖100%

package demo

import "testing"

func TestGetArea(t *testing.T) {
	areas := GetArea(40, 50)
	if areas != 2000 {
		t.Error("测试失败")
	}
}

func TestGetArea1(t *testing.T) {
	areas := GetArea(50, 40)
	if areas != 2000 {
		t.Error("测试失败")
	}
}
func BenchmarkGetArea(t *testing.B) {
	for i := 0; i < t.N; i++{
		GetArea(40, 50)
	}
}

执行命令:

go test -cover
PASS
coverage: 100% of statements
ok      _/home/hjy/TestCode/go/demo     0.001s


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值