语言学习 June 21, 2021

Go语言入门学习

Words count 73k Reading time 1:06 Read count 0

前言

我觉得Go语言就是C(有些格式要求)和python(有些语法的写法)的融合体,还夹杂着一点java的影子(调用名字大小写)。

一、环境安装

在菜鸟编程里有源码安装的说明:

https://www.runoob.com/go/go-environment.html

当然也可以直接命令行安装go的编译运行环境:

1
sudo apt  install gccgo-go

当装好了以后,会自动配置好环境变量等,然后我们就可以开始hello world了:

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
// 当前程序的包名
package main

// 导入其他包
import . "fmt"

// 常量定义
const PI = 3.14

// 全局变量的声明和赋值
var name = "gopher"

// 一般类型声明
type newType int

// 结构的声明
type gopher struct{}

// 接口的声明
type golang interface{}

// 由main函数作为程序入口点启动
func main() {
Println("Hello World!")
}

运行命令:

1
2
3
4
#生成二进制文件的形式
go build 1.go #得到1
#直接运行源文件的形式
go run 1.go

二、Go的基本语法

1、注释

其实和c语言的一模一样,看下代码就知道了:

1
2
//单行注释
/*整体注释*/

2、字符串连接

和python的一样,直接+即可

1
2
3
4
5
package main
import "fmt"
func main() {
fmt.Println("Google" + "Runoob")
}

3、关键字

下面列举了 Go 代码中会使用到的 25 个关键字或保留字

break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符:

append bool byte cap close complex complex64 complex128 uint16
copy false float32 float64 imag int int8 int16 uint32
int32 int64 iota len make new nil panic uint64
print println real recover string true uint uint8 uintptr
程序一般由关键字、常量、变量、运算符、类型和函数组成。

程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}。

程序中可能会使用到这些标点符号:.、,、;、: 和 …。

4、数据类型

有int、bool、字符串型、float和指针等

1
2
3
4
5
6
7
8
9
package main
import "fmt"

func main() {
var a int= 1.5
var b int = 2
var c string = "victor"
fmt.Println(a,b,c)
}

5、变量值的声明和定义

这里要记得,全局变量的话,可以声明不引用,但是函数体内的局部变量声明了就一定要引用,同时变量是可以省略类型和var的,编译器会智能识别出来,写法可以类似python,但是多了个:

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
var f = "king"
func main() {
var a float64= 1.5
const LENGTH int = 10
const hg = iota
var b,c,d =2,3,"victor"
e:=8
fmt.Println(a,b,c,d,e,LENGTH,hg)
}

6、运算符

常见的运算符和c没什么两样,这里略过,不同在于可以将if的()去掉,和python类似

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
package main
import "fmt"
var f = "king"
func main() {
var a float64= 1.5
const LENGTH int = 10
const hg = iota
var b,c,d =2,3,"victor"
e:=8
fmt.Println(a,b,c,d,e,LENGTH,hg)

aa:=10
bb:=30
cc:=(aa+bb/2*100-200)%75
cc^=0x76
fmt.Printf("this is %d\n",cc)
if cc>=10 {
fmt.Printf("good!")
}else if cc<2 {
fmt.Printf("no!!!")
}else{
fmt.Printf("heihei")
}

var ptr *int
fmt.Printf("type-->%T",ptr)

}
优先级 运算符
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||

7、条件语句

if和switch的常规操作,switch中的fallthrough是可以直接实现不判断下一个case继续执行的功能,特殊情况下有特殊用途。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
if cc>=10 {
fmt.Printf("good!")
if cc==100{
fmt.Printf("hhhhh")
}
}else if cc<2 {
fmt.Printf("no!!!")
}else{
fmt.Printf("heihei")
}

var ptr *int
fmt.Printf("type-->%T\n",ptr)

switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true")
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")
break
case true:
fmt.Println("4、case 条件语句为 true")
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}

var c2, c3 chan int
var i1, i2 int
c1 := make(chan int,10)
i1=10
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}

8、循环语句

这里的循环兼容了python和c,但是没有了while的关键字

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
i:=0
for i<10 { //传说中的while循环
i++
fmt.Printf("number %d\n",i)
}
str:=[]string{"google","baidu"}
for _,v := range str{//_表示不需要的值
fmt.Println(v)
}
lll:=0
for range str{ //所谓的单纯循环
fmt.Println(str[lll])
lll++
}
for i := range [100]int{}{ //按数量循环
fmt.Println(i)
}
mymap:=map[string]string{"a":"king","b":"victor","c":"yee"}
for k,v:= range mymap{ //键值对,针对map而言,map其实就是python中的字典概念
fmt.Printf("key-->%s and value-->%s\n",k,v)
}
delete(mymap,"a")//删除的意思
for k,v:= range mymap{
fmt.Printf("key-->%s and value-->%s\n",k,v)
}

9、函数定义与函数调用+指针和结构体

一般来说,main肯定是要有的,其他函数可以自行定义,问题不大;结构体和指针,感觉和c的其实没有什么大的区别

敲黑板,划重点

当要将结构体对象转换为 JSON 时,对象中的属性首字母必须是大写,才能正常转换为 JSON。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var f = "king"
type myfunc func(int) int //函数名作为参数的声明和定义
type register struct{
rax int
rbx int
}
func main() {
val,stringss:=max(8,108,"king")
fmt.Println(val,stringss)
vv:=testcallback(10,callback)//函数名作为参数
fmt.Printf("FuncCallBack-->%d\n",vv)
var reg register
changevalue(&reg,30)//普通结构体的写法,指针
fmt.Printf("rax-->%d and rbx-->%d\n",reg.rax,reg.rbx)
reg.changevalue2(70)//类似面向对象的写法,指针
fmt.Printf("rax-->%d and rbx-->%d\n",reg.rax,reg.rbx)
bv:=bibao() //闭包的写法,可以直接使用函数里面的变量
fmt.Println(bv())
fmt.Println(bv())
}

func max(a,b int,str string)(int,string){
var result int
if a>b{
result=a
}else{
result=b
}
fmt.Println(str)
return result,str
}
func testcallback(x int, f myfunc) int{
return f(x)
}
func callback(x int) int {
return x+7
}
func changevalue(reg *register,aa int){
reg.rax=aa+1
reg.rbx=aa+2
}

func (reg *register)changevalue2(aa int){
reg.rax=aa+1
reg.rbx=aa+2
}
func bibao() func() int{ //内联形式的闭包,匿名函数
i:=0
return func() int{
i++
return i
}
}

10、数组

踩坑:多维数组初始化或赋值时需要注意 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
//数组专题
{
mystr:=[20]string{"a","b","c","d","e",//实在需要换行的话
}
myint:=[100]int{0,1,2,3,4,5,6,10:99,75:77}
myarray := [][]int{{1,2},{3,4},{5,6},{7,8},{9,10}}
row1 := []int{11,12,1234}
myarray = append(myarray,row1)//二维数组添加元素方法
for i:=0;i<12;i++{
fmt.Println(mystr[i])
fmt.Println(myint[i])
if i<6 {
fmt.Println(myarray[i])
}
}
for j:=0;j<6;j++{//二维数组输出
for k:=0;k<2;k++{
fmt.Println(myarray[j][k])
}
}
for l:= range myarray{ //遍历数组中每个值
fmt.Println(myarray[l])
}
}

11、切片(动态数组概念)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ggg:=[]int{1,23,4,56,78,10,111,222,777,9999}
fff:=ggg[1:10]
printslice(fff)
hhh := make([]int,3,5) //空切片生成,len=0会导致copy失效!
printslice(hhh)
hhh = append(hhh,1,2,3,4)
hhh = append(hhh,fff...) //切片合并,...的妙用
printslice(hhh)
copy(hhh,fff)
printslice(hhh)
slicechange(hhh)
printslice(hhh)

func printslice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
func slicechange(x []int){
x[0]=777
}

可以实现下标切割,内存copy时不会越界。当把 slice 作为参数,本身传递的是值,但其内容就 byte* array,实际传递的是引用,所以可以在函数内部修改,但如果对 slice 本身做 append,而且导致 slice 进行了扩容,实际扩容的是函数内复制的一份切片,对于函数外面的切片没有变化。另外append会让与之相关的切片脱钩(要想不脱钩,只能通过&的方式去整)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 每次cap改变,指向array的ptr就会变化一次
s := make([]int, 1)
fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))

for i := 0; i < 5; i++ {
s = append(s, i)
fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))
}
fmt.Println("Array:", s)

//结果
len:1 cap: 1 array ptr: 0xc8200640f0
len:2 cap: 2 array ptr: 0xc820064110
len:3 cap: 4 array ptr: 0xc8200680c0
len:4 cap: 4 array ptr: 0xc8200680c0
len:5 cap: 8 array ptr: 0xc82006c080
len:6 cap: 8 array ptr: 0xc82006c080
Array: [0 0 1 2 3 4]

解释:每次cap改变的时候指向array内存的指针都在变化。当在使用 append 的时候,如果 cap==len 了这个时候就会新开辟一块更大内存,然后把之前的数据复制过去(实际go在append的时候放大cap是有规律的。在 cap 小于1024的情况下是每次扩大到 2 * cap ,当大于1024之后就每次扩大到 1.25 * cap 。所以上面的测试中cap变化是 1, 2, 4, 8),看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

12、接口

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//接口,指针传参才会影响值
type Animal interface{
call()
eat(x string) int
}

type Cat struct{
age int
height float64
}

type Dog struct{
age int
height float64
}

func (cat Cat) call(){
fmt.Println("miao miao~")
}

func (cat Cat) eat(x string) float64 {
if x=="fish" {
fmt.Println("Eating fish!")
cat.height += 7.0
}else{
fmt.Println("Don't eat!")
}
return cat.height
}

func (dog Dog) call(){
fmt.Println("wang wang~")
}

func (dog *Dog) eat(x string) float64 {
if x=="duck" {
fmt.Println("Eating duck!")
dog.height += 10.0
}else{
fmt.Println("Don't eat!")
}
return dog.height
}
func main(){
cat := Cat{1,10.5}
dog := new(Dog)
dog.age=3
dog.height=20.5
cat.call()
dog.call()
h1:=cat.eat("fish")
h2:=dog.eat("duck")
fmt.Println(h1)
fmt.Println(h2)
fmt.Println(cat.height)
fmt.Println(dog.height)
}

接口为结构体实现了函数调用的方法,已经具备类相关的特征了。

13、关于异常捕获

panic(抛出异常)、recover(恢复上一级函数调用栈)、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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package main

import (
"fmt"
)

func main() {
fmt.Println("外层开始")
defer func() {
fmt.Println("外层准备recover")
if err := recover(); err != nil {
fmt.Printf("%#v-%#v\n", "外层", err) // err已经在上一级的函数中捕获了,这里没有异常,只是例行先执行defer,然后执行后面的代码
} else {
fmt.Println("外层没做啥事")
}
fmt.Println("外层完成recover")
}()
fmt.Println("外层即将异常")
f()
fmt.Println("外层异常后")
defer func() {
fmt.Println("外层异常后defer")
}()
defer func() {
fmt.Println("外层异常后defer222222")
}()
}

func f() {
fmt.Println("内层开始")
defer func() {
fmt.Println("内层recover前的defer")
}()

defer func() {
fmt.Println("内层准备recover")
if err := recover(); err != nil {
fmt.Printf("%#v-%#v\n", "内层", err) // 这里err就是panic传入的内容
}
fmt.Println("内层完成recover")
}()

defer func() {
fmt.Println("内层异常前recover后的defer")
}()

panic("异常信息")

defer func() {
fmt.Println("内层异常后的defer")
}()

fmt.Println("内层异常后语句") //recover捕获的一级或者完全不捕获这里开始下面代码不会再执行
}

/*效果如下
外层开始
外层即将异常
内层开始
内层异常前recover后的defer
内层准备recover
"内层"-"异常信息"
内层完成recover
内层recover前的defer
外层异常后
外层异常后defer222222
外层异常后defer
外层准备recover
外层没做啥事
外层完成recover
*/
  • 1、panic 在没有用 recover 前以及在 recover 捕获那一级函数栈,panic 之后的代码均不会执行;一旦被 recover 捕获后,外层的函数栈代码恢复正常,所有代码均会得到执行;
  • 2、panic 后,不再执行后面的代码,立即按照逆序执行 defer,并逐级往外层函数栈扩散;defer 就类似 finally;
  • 3、利用 recover 捕获 panic 时,defer 需要再 panic 之前声明,否则由于 panic 之后的代码得不到执行,因此也无法 recover

所以如果我把f里面的recover去掉的话,效果就是f()后面的函数也不会继续执行:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
"fmt"
)

func main() {
fmt.Println("外层开始")
defer func() {
fmt.Println("外层准备recover")
if err := recover(); err != nil {
fmt.Printf("%#v-%#v\n", "外层", err) // err已经在上一级的函数中捕获了,这里没有异常,只是例行先执行defer,然后执行后面的代码
} else {
fmt.Println("外层没做啥事")
}
fmt.Println("外层完成recover")
}()
fmt.Println("外层即将异常")
f()
fmt.Println("外层异常后")
defer func() {
fmt.Println("外层异常后defer")
}()
defer func() {
fmt.Println("外层异常后defer222222")
}()
}

func f() {
fmt.Println("内层开始")
defer func() {
fmt.Println("内层recover前的defer")
}()

/*defer func() {
fmt.Println("内层准备recover")
if err := recover(); err != nil {
fmt.Printf("%#v-%#v\n", "内层", err) // 这里err就是panic传入的内容
}
fmt.Println("内层完成recover")
}()
*/

defer func() {
fmt.Println("内层异常前recover后的defer")
}()

panic("异常信息")

defer func() {
fmt.Println("内层异常后的defer")
}()

fmt.Println("内层异常后语句") //recover捕获的一级或者完全不捕获这里开始下面代码不会再执行
}
/*效果如下
外层开始
外层即将异常
内层开始
内层异常前recover后的defer
内层recover前的defer
外层准备recover
"外层"-"异常信息"
外层完成recover
*/

通俗理解就是,f函数中defer的func会在panic抛出异常后,倒着执行,如果往上走的过程中,defer里面有recover的话,那么上一级的函数调用main中就会执行当前f函数后面的defer,也是倒着逆序执行。如果没有异常的话,就是等函数全部执行完(或者return 后)再执行defer中的信息,一样逆序执行,简单说就是这样:

1
2
3
返回值 = xxx
调用defer函数(这里可能会有修改返回值的操作)
return 返回值

它本身有个简单的Error的接口可以实现简单的报错处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (cat Cat) Error() {
fmt.Println("cat don't eat duck!!!")
}

func (cat Cat) eat(x string) float64 {
if x=="fish" {
fmt.Println("Eating fish!")
cat.height += 7.0
}else if x=="duck"{
cat.Error()
}else{
fmt.Println("Don't eat!")
}
return cat.height
}

cat.eat("duck")

14、并发执行和通道的知识

go里面可以实现轻量级的线程操作,即goroutain,然后加入了管道的概念,就会有阻塞的情况出现,如果不手动加close的话,就会直接报错。

通道遵循先进先出原则。

不带缓冲区的通道在向通道发送值时,必须及时接收,且必须一次接收完成。

而带缓冲区的通道则会以缓冲区满而阻塞,直到先塞发送到通道的值被从通道中接收才可以继续往通道传值。就像往水管里推小钢珠一样,如果钢珠塞满没有从另一头放出,那么这一头就没法再往里塞,是一个道理。例如上面的例子,最多只能让同时在通道中停放2个值,想多传值,就需要把前面的值提前从通道中接收出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//并发和管道
{
ch:=make(chan int,10)
go caculatesum(10,ch)
// for i:= range ch{
// fmt.Println(i)
// }
for i:=0;i<10;i++{
x := <-ch
fmt.Println(x)
}
}

func caculatesum(x int,c chan int){
for i:=0;i<10;i++{
x += 1
c <- x
time.Sleep(1000*time.Millisecond)
}
close(c)
}

管道还是很有用的!

三、go程序调试

0%