go指南 - 官网

官网的文档,应该是最好的入门了吧

基础

每个 Go 程序都是由包构成的。

程序从 main 包开始运行。

一般包名与导入路径的最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。

import (
    "fmt"
    "math"
)
// 上面这种导入方式更好
import "fmt"
import "math"

如果一个名字以大写字母开头,那么它就是已导出的

如,math.pi就是未导出的,就没有定义,不能访问和调用。

math.Pi就导出了。导入一个包,只能引用已经导出的名字。

类型在变量名之后。

为什么要这么做?

c的类型是定义在前面的。如果函数的参数是函数,或者返回值是函数,那么就会难以理解

int (*fp)(int (*ff)(int x, int y), int b)
int (*(*fp)(int (*)(int, int), int))(int, int)

go从后置类型的定义进化而来

x: int
p: pointer to int
a: array[3] of int

变成
x int
p *int
a [3]int

用go写就是

f func(func(int,int) int, int) int
f func(func(int,int) int, int) func(int, int) int

但是指针的*是前置的

var p *int
x = *p

// 而不是
var p *int
x = p*

因为可能被理解为乘法

变量初始化如果赋值了,那么可以省略类型。具体被赋值成啥,看精度。如int,float64,complex128等等

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。

:= 结构不能在函数外使用,只能用var

基本类型

bool

string

int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名 // 表示一个 Unicode 码点

float32 float64

complex64 complex128

变量声明也可以“分组”成一个语法块。

没有明确初始值的变量声明会被赋予它们的 零值

0,false,“”

类型转换

流程控制

for

Go 只有一种循环结构:for 循环。

基本的 for 循环由三部分组成,它们用分号隔开:

  • 初始化语句:在第一次迭代前执行。仅在for作用域中可见。

  • 条件表达式:在每次迭代前求值

  • 后置语句:在每次迭代的结尾执行

可以省略分号变成while,甚至省略条件变成死循环。

if

不需要小括号,可以在前面加一个分号来执行简单语句。作用域就在if的范围内。

switch

相当于一大串的if-else。只运行选定的case,因为自动提供了break

switch 的 case 语句从上到下顺次执行,直到匹配成功时停止。

不加条件的switch能看起来更清楚。

defer

defer 语句会将函数推迟到外层函数返回之后执行。【函数结束前,执行这个defer内容】

defer很多,就会压入堆栈中。后进先出

深入defer

更多类型

指针

指针保存了值的内存地址。go没有指针运算。

  • & 操作符会生成一个指向其操作数的指针。 &i获得i的内存地址,如0xc000090000

  • * 操作符表示通过指针指向变量的值。 *后面要加上地址,这能获取这个地址的值,如*(&i)=i,都是i的值。

p := &i中的pi的地址,*p是顺着地址找到了i的值

结构体

一个结构体(struct)就是一组字段(field)。

【感觉是数组的升级版,数组只能全部是一个类型,而结构体可以不同东西有不同类型】

数组

类型 [n]T 表示拥有 nT 类型的值的数组。

var a [10]int,表示将变量 a 声明为拥有 10 个整数的数组。

数组不能改变大小。长度是其类型的一部分

【感觉channel就是数组做出来的】

切片

引用一部分数组。它并不存储任何数据,它只是引用了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。【直接通过指针改变了对应的值】

类型 []T 表示一个元素类型为 T 的切片。

a[1:4]包含数组 a 中下标从 1 到 3 的元素。和python一样,不写就默认是上下界。

除了用[1:4]来切片,还可以用无长度的数组来创建

切片的长度和容量

切片s,长度是len(s),容量是cap(s)

左指针改容量,右指针不改容量。因为切片的index必须是一个非负数,所以取不到-1,所以左指针就规定了容量。

容量,也就是可能取到的长度。

长度是切片的内容长度,容量是从左边指针的切片开始算起的。

参考

image-20190726131521502

切片的零值

切片的零值是 nil。nil 切片的长度和容量为 0 且没有底层数组。

make创建切片

切片追加元素append

参考

append 内建函数将元素追加到切片的末尾。 若它有足够的容量,其目标就会重新切片以容纳新的元素。否则,就会分配一个新的基本数组。 append 返回更新后的切片。因此必须存储追加后的结果,通常为包含该切片自身的变量:

作为一种特殊的情况,将字符追加到字节数组之后是合法的,就像这样:

Go 切片:用法和本质

Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。

当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。【js当中也是这样,重新赋值一个值,如string,相当于照样新建了一个值。与原来的毫无关系。js参考

(为了避免复制数组,你可以传递一个指向数组的指针,但是数组指针并不是数组。)

可以将数组看作一个特殊的struct,结构的字段名对应数组的索引,同时成员的数目固定。

切片的内幕

一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。

image-20190726153457126

make([]byte, 5)相当于

image-20190726153604851

s = s[2:4]

image-20190726153710804

切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。【改变头的指针地址】

通过一个新切片修改元素会影响到原始切片的对应元素。

切片的生长

要增加切片的容量必须创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。

可能的陷阱

切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。

这样的话,有时候可能会因为一个小的内存引用导致保存所有的数据。

【简言之要用切片来操作,而非操作整个数组。感觉切片的出现就是为了避免整个数组的操作】

range

for 循环的 range 形式可遍历切片或映射。

每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

可以用_来忽略

练习:切片

实现 Pic。它应当返回一个长度为 dy 的切片,其中每个元素是一个长度为 dx,元素类型为 uint8 的切片。当你运行此程序时,它会将每个整数解释为灰度值(好吧,其实是蓝度值)并显示它所对应的图像。

图像的选择由你来定。几个有趣的函数包括 (x+y)/2, x*y, x^y, x*log(y)x%(y+1)

(提示:需要使用循环来分配 [][]uint8 中的每个 []uint8;请使用 uint8(intValue) 在类型之间转换;你可能会用到 math 包中的函数。)

作者:BigManing 来源:CSDN 原文:https://blog.csdn.net/qq_27818541/article/details/54346106

image-20190726162610193

感觉x^y最好看

image-20190726163632583

映射map

将键映射到值。零值为nil,没有键也不能添加键。

练习:映射

实现 WordCount。它应当返回一个映射,其中包含字符串 s 中每个“单词”的个数。函数 wc.Test 会对此函数执行一系列测试用例,并输出成功还是失败。

你会发现 strings.Fields 很有帮助。

函数

函数也是值,可以像值一样被传递。

闭包

练习:斐波纳契闭包

https://blog.csdn.net/qq_27818541/article/details/54347998

方法和接口

方法

就是给结构体加一个方法。

这些都是在下面增加方法。因此处理的都是输入的值的副本

如果用指针,就是在直接操作值本身。

函数如果需要传入指针(输入有*),那么必须传入(参数&,不然报错)

而type下面的方法就不需要,解释器会默认处理好。

一般都是传地址,复制值成本太高

接口

一个防御输入值为nil的方法:

如果值是nil,那么调用其底下方法,就会产生panic报错。

因为不知道该用哪个类型的方法。【可能因为nil下面并没有这个方法】

类型断言

语法:t := i.(T),断言iT类型。

类型选择

从几个类型断言中选择分支的结构。

就是一种根据不同输入类型来选择操作的switch结构。类型用type表示

stringer

fmt默认的打印方法,是调用结构体下面的String() 方法。

因此可以通过修改String()的方法,来修改该结构体的打印值。

【感觉这种能操纵默认方法的操作,非常有毒】

练习:Stringer

输入IPAddr{1, 2, 3, 4} 打印为 "1.2.3.4"

go似乎不能直接对数组进行操作,这是一个不可变的东西。而又查到了,可以对string进行splitjoin

因此一开始的想法,是把数组一个个拿出来转成string,然后再进行splitjoin成想要的地址样子。

不过bytestring,能查到的方法,只有在前面加一个string(),如string(p)。但这个仅对[]byte类型有用,而这里的p的类型是IPAddr,所以就不能用。

查到了这个方法来解决:

作者:BigManing 来源:CSDN 原文:https://blog.csdn.net/qq_27818541/article/details/54347335

感觉加点可以不用那么麻烦,最后删掉一位字符串即可

不过go里面的数组都是值,是不可变的,因此字符串拼接的操作,其实是创建了一个新的字符串,其值是两个字符串拼接的而已。包括最后的切一位,也是生成了一个新的字符串。

也就是说,循环了len(p)次数,意味着产生了len(p)个字符串,不过之前的都被废弃了,只剩下最后一个而已。换言之这对内存成本很高。

然后发现了一个绕过拼接字符串的方法。

https://blog.csdn.net/u013564276/article/details/49804633

主要是Sprintf这个函数的操作。参考

错误

error 类型是一个内建接口

error 为 nil 时表示成功;非 nil 的 error 表示失败。

练习:错误

https://blog.csdn.net/qq_27818541/article/details/54347939

https://linkscue.com/2018/02/28/go-tour-practice-error/

ErrNegativeSqrt(x)是把xfloat64转成ErrNegativeSqrt

作为error输出的时候,会优先调用Error()来打印。然而这个函数返回的也是一个打印,因此如果用e本身,就会触发死循环。得转换一下格式才行。

reader

一次读取多少字节

练习:reader

不是很懂这个😂😂

练习:rot13Reader

就是用reader解密咯。加密方法是ROT13。用一遍加密,再用一遍解密。因为就是把字母替换为其13位后面的字母。

https://blog.csdn.net/qq_27818541/article/details/54379030

图像

image包

练习:图像

image-20190822171738736

并发

go routine

goroutine是由 Go 运行时管理的轻量级线程。

会启动一个新的 Go 程并执行

在新的 Go 程中只负责执行 f ,其他的求值发生在当前的 Go 程中。

Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。

信道

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。

(“箭头”就是数据流的方向。)

和映射与切片一样,信道在使用前必须创建:

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

如果计算量小的话,就会在第二个协程开启前,就已经把第一个协程内容删掉了

带缓冲的信道

make初始化信道,第二个参数是缓冲长度

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

【满了不让塞,空了不让拿】

range 和 close

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。

接收者可以通过为接收的第二个参数,来判断信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

之后 ok 会被设置为 false

循环 for i := range c 会不断从信道接收值,直到它被关闭。

注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

select

select 语句使一个 Go 程可以等待多个通信操作。

select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

可以用struct{}改写,这样省内存空间。

默认选择

为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:

练习:等价二叉查找树

树的结构是这样的:

要写一个walk函数,来一个个把树的内容传到信道里面,然后用same函数来比较两个树是否一样。

// 原文链接:https://blog.csdn.net/qq_27818541/article/details/54411990

sync.Mutex

若想避免冲突,保证每次只有一个 Go 程能够访问一个共享的变量,从而

这里涉及的概念叫做 互斥(mutualexclusion) ,我们通常使用 互斥锁(Mutex)* 这一数据结构来提供这种机制。

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  • Lock

  • Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。 我们也可以用 defer 语句来保证互斥锁一定会被解锁

web爬虫

Last updated

Was this helpful?