惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
小众软件
小众软件
J
Java Code Geeks
I
InfoQ
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
L
LangChain Blog
博客园 - 司徒正美
量子位
Y
Y Combinator Blog
C
Check Point Blog
T
Tailwind CSS Blog
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
N
Netflix TechBlog - Medium
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Fortinet All Blogs
云风的 BLOG
云风的 BLOG
A
About on SuperTechFans
B
Blog RSS Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
V
V2EX
阮一峰的网络日志
阮一峰的网络日志

Inno' Blog

将 what-digit-you-write 部署到 Heroku上 | Inno' Blog 论文浅析:GpSense: A GPU-friendy method for common-sense subgraph matching in massively parallel architectures Github项目clone速度过慢?试试这招 | Inno' Blog Hadoop分布式搭建全流程记录 | Inno' Blog Shell编程学习记录 | Inno' Blog 重学MySQL之视图 | Inno' Blog 重学MySQL之查询练习 | Inno' Blog 重学MySQL之事务 | Inno' Blog 重学MySQL之范式 | Inno' Blog 重学MySQL之基本操作和约束 | Inno' Blog 为你的Web应用提供HTTPS服务 | Inno' Blog 一个关于切片的小问题 | Inno' Blog 关于英译中的个人技巧 | Inno' Blog 不知不觉已经写够了十万行代码 | Inno' Blog YARN 架构 | Inno' Blog HDFS 架构 | Inno' Blog 正则中关于环视(lookaround)的小例子 | Inno' Blog 设计模式(十八):访问者模式 | Inno' Blog 如何借助Github进行协同开发 | Inno' Blog
Go语言学习笔记 | Inno' Blog
Inno Fang · 2020-02-11 · via Inno' Blog

Go 语言入门

变量定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a int 
var b int = 10
var c, d int
var e, f int = 3, 4
var g, h, i, j = 5, 6, true, "def"
k, l, m := 7, 8.1, false, "def"

var (
aa = 1
bb = 2.2
cc = "define"
dd = true
)


TIPS 运行 GO 的 main 函数,所在包的包名也要是 main

1
2
3
4
5
6
7
package main  

import "fmt"

func main() {
fmt.Println("Hello World")
}

内建变量

  • bool 布尔型
  • string 字符串型
  • (u)int, (u)int8, (u)int16, (u)int32, (u)int64, (u)intptr 数字表示int位数,不加就默认为当前计算机位数,ptr 表示指针,u表示无符号
  • byte 字节型,8位1字节
  • rune Go语言的字符型,32位4个字节
  • float32, float64 浮点型,数字为位数
  • complex64, complex128 复数类型,前者实部和虚部都是 float32,后者两个都是 float64

强制类型转换

Go语言类型转换是强制

常量的定义

与变量的定义有很多相似的地方,不过要用 const 代替 var

定义常量的时候可以指定类型,也可以不指定类型,若不指定类型,就是个位置类型的文本,在使用的时候会进行推断

1
2
3
4
const a, b = 3, 4
var c = int
c = int(math.Sqrt(a * a + b * b))

定义枚举类型

Go 语言没有枚举值,只能用 const 来模拟

1
2
3
4
5
6
7
8
9
10
const (
Mon = iota
Tue
Wed
Thr
Fri
Sat
Sun
)
fmt.Println(Mon, Tue, Wed, Thr, Fri, Sat, Sun)

iota 是一个预先定义的标识符,在 const 表达式中(通常在括号内),用来表示当前 const 规范的非类型整数顺序数,索引为 0

条件语句

if

1
2
3
4
5
6
7
const filename = "in.txt"
contents, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}

Go的条件语句的特点

  1. if 条件里可以赋值,且赋值的变量作用域只在这个 if 语句里
  2. 条件语句部分不需要括号,并且可以用 ; 来分隔多个条件

上面的例子可以进一步修改为

1
2
3
4
5
if contents, err := ioutil.ReadFile(filename); err == nil {
fmt.Printf("%s\n", contents)
} else {
fmt.Println(err)
}

switch

1
2
3
4
5
6
7
8
9
10
11
12
func eval(a, b int, op string) int {
var result int
switch op {
case "+": result = a + b
case "-": result = a - b
case "*": result = a * b
case "/": result = a / b
default:
panic("unsupported operator:" + op)
}
return result
}

Go 语言中 switch 的特点:

  1. swtich 会自动 break,除非使用 falllthrough
  2. switch 后可以没有表示,在 case 中写表达式也可以

循环语句

for

去其他语言的 for 循环的区别在于没有 ( ),并且三个部分各自都可以省略

1
2
3
4
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}

Go 语言中没有 while,如果把 for 玄幻的初始化和递增条件省略,就类似于 while

如果把三个部分都省略,那么就相当于死循环

1
2
3
for {
...
}

函数

多个返回值

Go 的函数可以返回多个值

1
2
3
func div(a, b int) (int, int) {
return a / b, a % b
}

此外,还可以给返回值命名,可以增强代码可读性,也可以为编辑器的补全提供帮助。

若指明了返回值名称,则可以对返回值进行直接赋值,最后只需要一个 return 即可(该用法仅用于非常简单的函数,否则影响可读性)

1
2
3
4
5
func div(a, b int) (q, r int) {
q = a / b
r = a % b
return
}

函数式编程

1
2
3
4
5
6
func apply(op func(int, int) int, a, b int) int {
fmt.Printf("Calling %s with (%d, %d)\n",
reflect.ValueOf(op).Pointer(runtime.FuncForPC(p).Name()),
a, b)
return op(a, b)
}

使用

1
2
3
apply(func(a int, b int) int {
return a * b
}, 2, 3)

可变参数列表

Go 语言没有默认参数函数重载,但有可变参数列表

1
2
3
4
5
6
7
func sum(numbers ...int) int {
s := 0
for i := range numbers {
s += numbers[i]
}
return s
}

指针

Go 语言的指针不能进行运算,这也是他简单的地方
Go 语言只有值传递一种方式(与值传递对应的是类似与 C++ 的引用传递,这个才 Go 中是没有的)

1
2
3
func swap(a, b *int) {
*a , *b = *b, *a
}

数组

1
2
3
4
var arr1 [5]int 			
arr2 := [3]int{1, 3, 5}
arr3 := [...]int{1, 3, 4}
var grid [3][4]int

数组的遍历,可以使用 range 关键字,可以获得下标和值

1
2
3
4
arr := [...]int{2, 4, 6, 8, 10}
for i, v := range arr {
fmt.Println(i, v)
}

如果只需要值不需要下标,则可以用 __ 代替

1
for _, v := range arr { ... }

若只要下标,则可以直接写成

1
for i range arr { ... }

Go 中的数组是值类型,也就是说数组传递给函数后,会拷贝数组内容,修改数组的值不会改变原数组的内容(大部分语言的数组都是引用传递,而 Go 则是值传递)。此外[10]int[20]int 是不同类型,也就是说实参数组的长度要与形参数组的长度相一致,否则会报错。

要实现引用传递,则需要用到指针

1
2
3
4
5
func foo(arr *[5]int) { ... }
func main() {
var arr[5]int
foo(&arr)
}

可见使用数组还是相当麻烦的,所以在 Go 中使用的更多的是切片(Slice)

切片

1
2
3
4
5
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Println(arr[2:6])
fmt.Println(arr[:6])
fmt.Println(arr[2:])
fmt.Println(arr[:])

切片生成的是数组的一个视图,修改切片会改变原数组,在函数中传递数组时,若要方便修改原数组,可以使用切片

1
2
3
4
5
func foo(arr []int) { ... } 
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
foo(arr[:])
}

  1. slice 可以向后扩展,但是不能向前扩展
  2. s[i] 不可以超越 len(s),向后扩展不可以超越底层数组 caps(s)
  3. 向slice中添加元素,如果长度没超过 cap,则会覆盖原数组的内容,若超过cap,系统则会重新分配更大的底层数组
  4. 由于值传递,使用 append 有可能会修改 slice 的 len 或 cap ,因此必须接受 append 的返回值,即 newSlc := append(slc, newValue)

切片的创建方式

1
2
3
s1 := []int{2, 4, 6, 8}		
s2 := make([]int, 16)
s3 := make([]int, 10, 32)

切片的操作

切片的复制

1
copy(s1, s2) 

切片元素的删除

1
2
3
4
5
6
7
s = append(s[:3], [4:]) 

front := s[0]
s = s[1:]

tail := s[len(s) - 1]
s = s[:len(s)-1]

Map

定义形式如下

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
30
31
32
33
34
35
36
37
map[K]V
map[K1]map[K2]V


m := map[string]string {
"name" : "Inno",
"id" : "1234",
"gender" : "male",
}
fmt.Println(m)


m2 = make(map[string]string)
var m3 = map[string]string


for k, v := range m {
fmt.Println(k, v)
}


name := m["name"]
fmt.Println(name)


if age, ok := m["age"]; ok {
fmt.Println("age:", age)
} else {
fmt.Println("There is not a key called age!")

}


delete(m, "gender")


len(m)
  • map 使用哈希表,必须可以比较相等
  • 除了 slice、map、function 的内建类型都可以作为key
  • 自建类型(Struct 类型)也可以作为 key,前提是没有 slice、map、function

面向对象

Go 语言面向对象仅支持封装,不支持继承和多态;Go语言没有class,只有 struct

结构的创建和简单使用

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
30
31
32
33
34
package main

import "fmt"

type treeNode struct {
value int
left, right *treeNode
}


func createTreeNode(value int) *treeNode {
return &treeNode{value: value}
}

func main() {
var root treeNode



root = treeNode{value: 3}
root.left = &treeNode{}
root.right = &treeNode{5, nil, nil}
root.right.left = new(treeNode)
root.left.right = createTreeNode(2)


nodes := []treeNode {
{value: 3},
{},
{6, nil, nil},
}
fmt.Println(nodes)
}

Q 在工厂方法中返回的局部变量的地址,该局部变量是创建在堆上还是在栈上?

A 因为 Go 语言有垃圾回收机制,具体是在堆上还是在栈上,由编译器来决定。若是函数中的局部变量,那么建在栈上;若将局部变量的地址返回给外部调用,那么这个局部变量就会键在堆上,并参与垃圾回收,当不再使用时,就会被回收掉。所以退出函数后,局部变量就会被销毁,这是不一定的。

为结构定义方法

1
2
3
func (node treeNode) print() {
fmt.Print(node.value)
}
  • (node treeNode) 显示定义和命名方法接收者,随便叫什么都可以

因为在Go语言中都是值传递,那么如果要在函数内修改值的话,值传递是无法生效的,因此可以使用指针作为方法接收者

1
2
3
func (node *treeNode) setValue(value int) {
node.value = value
}
  • 只有使用指针才可以改变结构内容
  • nil 指针也可以调用方法

值接收者 VS 指针接收者

  1. 要改变内容时必须使用指针接收者
  2. 结构过大时也考虑使用指针接收者(因为值接收者是对值的拷贝,结果过大则消耗越大)
  3. 一致性:如果有指针接收者,最好都是指针接收者(一个建议,保持一致有利于阅读以及代码维护)
  4. 值接收者 是 Go 语言特有的
  5. 值/指针接收者均可接收值/指针

封装

名字一般使用CamelCase,首字母大写为 public,首字母小写为 private。

封装是对包而言的,每个目录可以有很多个包,但只能有一个main包,main 包包含可执行入口。

为结构定义的方法必须在同一个包内,但可以是不同文件。

扩充类型

  1. 使用别名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type Queue []int 


    func (q *Queue) Push(v int) {
    *q = append(*q, v)
    }

    func (q *Queue) Pop() int {
    head := (*q)[0]
    *q = (*q)[1:]
    return head
    }
  2. 使用组合

利用组合为之前的 treeNode对象扩展一个遍历方法

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

type myTreeNode struct {
node *treeNode
}

func (myNode *myTreeNode) postOrder() {
if myNode == nil && myNode.node == nil {
return
}
left := myTreeNode{myNode.node.left}
right := myTreeNode{myNode.node.right}

left.postOrder()
right.postOrder()
myNode.node.print()
}

func main() {
var root treeNode


myRoot := myTreeNode{&root}
myRoot.postOrder()
fmt.Println()
}

接口

接口的定义

使用者定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)


type Retriever interface {
Get(url string) string
}


func download(r Retriever) string {
return r.Get("https://www.baidu.com")
}

func main() {
var r Retriever

fmt.Println(download(r))
}

实现接口

在接口所在目录下新建目录,创建新文件用来实现接口方法,当前目录结构为

1
2
3
4
LearnGo>innofang.io>innofang>interfaces
>real
|- main.go // 接口实现所在位置
|- main.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
package real

import (
"time"
"net/http"
"net/http/httputil"
)

type Retriever struct {
UserAgent string
TimeOut time.Duration
}


func (r Retriever) Get(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}

result, err := httputil.DumpResponse(resp, true)
resp.Body.Close()

if err != nil {
panic(err)
}

return string(result)
}

使用接口实现

1
2
3
4
5
func main() {
var r Retriever
r = real.Retriever{}
fmt.Println(download(r))
}

接口里面有什么

接口变量里面的内容有两种情况:

  1. 实现者的类型和实现者的值
  2. 实现者的类型和实现者的指针,其中指针指向实现者

具体是哪个,可以自由选择,因此在使用接口变量的时候,不要使用接口的地址,因为接口内部包含有指针

  • 接口变量自带指针
  • 接口变量同样采用值传递,几乎不需要使用接口的指针
  • 指针接收者实现只能以指针方式使用;值接收者都可

若接口方法实现时使用了指针接收者,那么在定义该接口实习时,需要取接口地址

1
2
3
4

func (r *Retriever) Get(url string) string {
...
}

那么在定义该接口实现者时,就需要使用取地址符

1
2
3
4
5
func main() {
var r Retriever
r = &real.Retriever{}
fmt.Println(download(r))
}

查看接口变量

再定义一个接口实现者

1
2
3
4
5
6
7
8
9
10
package mock

type Retriever struct {
Contents string
}


func (r Retriever) Get(url string) string {
return r.Contents
}

查看接口变量有两种方式

Type Switch

1
2
3
4
5
6
switch v := r.(type) { 
case mock.Retriever:
fmt.Println("Contents: ", v.Contents)
case *real.Retriever:
fmt.Println("UserAgent: ", v.UserAgent)
}

Type Assertion

1
2
3
4
5
6
7
8
9
10
11
12
realRetriever := r.(*real.Retriever) 
fmt.Println(realRetriever.TimeOut)





if mockRetriever, ok := r.(mock.Retriever); ok {
fmt.Println(mockRetriever.Contents)
} else {
fmt.Println("not a mock")
}

表示任何类型interface{}

1
2
3
4
5
6
7
8
9
10
11
type Quue []interface{} 

func (q *Queue) Push(v interface{}) {
*q = append(*q, v)
}

func (q *Queue) Pop() interface{} {
head := (*q)[0]
*q = (*q)][1:]
return head
}

接口的组合

假设有另一个结构体 Poster,现在要把含有 Get 方法的 Retriever 和含有 Post 方法的 Poster 进行组合

1
2
3
4
5
func RetrieverPoster interface {
Retriever
Poster

}

现在对于实现者来说,他可以自己实现 Post 方法和 Get 方法了

使用者不管定义者如何定义,它只管实现它要的方法,不需要关心使用的是什么接口

只要是一个类型就可以实现接口,这也是 Go 语言灵活的地方

defer 调用

何时使用 defer 调用

  • Open/Close
  • Lock/Unlock
  • PrintHeader/PrintFooter

被 defer 修饰的语句会确保在函数结束时发生,无论这个程序是否被提前终止,如果有多个 defer 会有一个栈来存储语句,采用先进后出的顺序执行语句

错误处理

1
2
3
4
5
6
7
8
9
file, err := os.Open("abc.txt")
if err != nil {

if pathError, ok := err.(*os.PathError); !ok {
panic(err)
} else {
fmt.Println("%s, %s, %s\n", pathError.Op, pathError.Path, pathError.Err)
}
}

panic

  • 停止当前函数执行
  • 一直向上返回,执行每一层的 defer
  • 如果没有遇见 recover,程序退出

recover

  • 仅在 defer 调用中使用
  • 获取 panic 的值
  • 如果无法处理,可重新 0panic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"errors"
"fmt"
)

func tryRecover() {
defer func() {
r := recover()
if err, ok := r.(error); ok {

fmt.Println("Error occured:", err)
} else {

panic(fmt.Sprint("I don't know what to do: %v", r))
}
}()
panic(errors.New("this is an error"))
}

func main() {
tryRecover()
}

表格驱动测试

  • 分离的测试数据和测试逻辑
  • 明确的出错信息
  • 可以部分失败
  • Go 语言的语法使得我们更易实践表格驱动测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Test(t *testing.T) {
tests := []struct {
a, b, c int32
}{
{1, 2, 3},
{0, 2, 2},
{0, 0, 0},
{-1, 1, 0},
{math.MaxInt32, 1, math.MinInt32},
}

for _, test := range tests {
if actual := add(test.a, test.b); actual != test.c {

...
}
}
}

代码覆盖率

在测试文件所在目录下执行

1
2
go test -coverprofile=c.out 
go tool cover -html=c.out

性能测试

1
2
3
4
5
6
7
8
9
10
11
func Becnchmark(b *testing.B) {
testCase := ...
ans := ...

for i := 0; i < b.N; i++ {
actual := testedFunc(testCase)
if actual != ans {
b.Errorf("...", ...)
}
}
}

命令行在测试文件所在目录下执行

1
go test -bench .

性能调优

在测试文件所在目录下执行

1
go test -bench . -cpuprofile cpu.out

生成了二进制文件 cpu.out,接着使用命令

1
go tool pprof cpu.out

进入交互式命令行,可以使用 help 查看,最简单的方式是输入 web 查看二进制文件。内容上,看到的方框越大,说明耗时越久,优化方框大的部分可以使程序性能更有。

Goroutine

协程

  • 轻量级“线程”
  • 非抢占式多任务处理,由协程序主动交出控制权
  • 编译器/解释器/虚拟机层面的多任务
  • 多个协程可能在一个或多个线程上运行

因为是非抢占式的 ,若不切换协程,则会一直执行一个协程(io操作会自动切换协程)。因此对于自定义协程,需要注意有没有手动交出控制权

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var a [10]int
for i := 0; i < 10; i++ {
go func(i int) {
for {
a[i]++
runtime.Gosched()
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}

goroutine 的定义

  • 任何函数只需要加上 go 就能送给调度器运行
  • 不需要在定义时区分是否是异步函数
  • 调度器在合适的点进行切换
  • 调试时,在命令行使用 -race 来检测数据访问冲突 go run -race goroutine.go

goroutine 可能的切换点

  • I/O, select
  • channel
  • 等待锁
  • 函数调用(有时)
  • runtime.Gosched()

只是参考,不能保证切换,不能保证在其他地方不切换

通道

指的是 goroutine 之间的通道,可以让 goroutine 之间互相通信。每个通道都有与其相关的类型,即通道允许传输的数据类型。通道的零值为 nil,需要使用内置 make 方法来定义

1
2
3
4

var 通道名 chan 数据类型

通道名 = make(chan 数据类型)

通道的注意点

Channel 通道在使用的时候,有以下几个注意点:

  1. 用于 goroutine,传递消息
  2. 通道,每个都有相关联的数据类型,nil chan 是无法使用的
  3. 使用通道传递数据:<-
    1. chan <- data,发送数据到通道。向通道中写
    2. data <- chan,从通道中获取数据。从通道中读数据
  4. 阻塞
    1. 发送数据:chan <- data,阻塞,直到另一条 goroutine 读取数据来接触阻塞
    2. 读取数据:data <- chan,阻塞,知道另一条 goroutine 写入数据来接触阻塞
  5. Channel 是同步的,意味着同一时间只有一条 goroutine 来操作

通道是 goroutine 之间的连接,所以通道的发送和接收必须处在不同的 goroutine 中

1
2
data := <- a 
a <- data

定义方式

1
2
3
4
5
6
7
8
9
10
11

c := make(chan int)
go func() {
for {
n := <- c
fmt.Println(n)
}
}()
c <- 1
c <- 2
time.Sleep(time.Millisecond)

关闭 channel

channel 可以被发送方 close,但是 channel 即使被 close 了,也会接收数据,只不过收到的是 zeroValue

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
func worker(id int, c chan int) {
for {










for n := range c {
fmt.Printf("worker %d received %c\n", id, n)
}
}
}


func channelClose() {
c := make(chan int, 3)
go worker(0, c)
c <- 'a'
c <- 'b'
c <- 'c'
c <- 'd'
close(c)
time.Sleep(time.Millisecond)
}

Select

select 是 Go 中的一个控制结构,select 语句类似于 switch 语句,但是 select 会随机执行一个可执行的 case。如果没有 case 可运行,若设置了 default 则会执行 default,否则它将阻塞,直到有 case 可执行。语法结构上与 switch 语句很相似

1
2
3
4
5
6
7
8
select {
case communication clause:
statement(s)
case communication clause:
statement(s)
default:
statement(s)
}
  • 每个 case 都必须是一个通信
  • 所有 channel 表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行
  • 若此刻没有 case 语句可以执行时,若有default语句,则执行该语句;否则 select 将阻塞,知道有某个通信可以运行,Go 不会重新对 channel 或值进行求值
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 main() {
ch1 := make(chan int)
ch2 := make(chan int)

go func() {
time.Sleep(1 * time.Second)
ch1 <- 100
}()

select {
case num1 := <-ch1:
fmt.Println("Read from ch1:", num1)
case num2, ok := <-ch2:
if ok {
fmt.Println("Read from ch2:", num2)
} else {
fmt.Println("ch2 has been closed")
}
case <-time.After(1 * time.Second):
fmt.Println("After 3 seconds")
default:
fmt.Println("default ...")
}
}