残月的技术日志

残月的技术日志

GO 语言快速上手

2024-03-16

本笔记仅适用与已经熟练掌握一门及以上编程语言者快速掌握GO语言

GO语言是由Google支持的开源编程语言。它易于学习,适合团队使用

准备动作

安装GO环境

下载GO并解压

https://golang.google.cn/dl/

我的操作系统是Linux,Windows有所区别

修改环境变量,这里的 GOROOT 根据实际情况调整

底部添加

export GOROOT=/data/home/canyue/App/go
export GOPATH=/data/home/canyue/go
export PATH=$GOROOT/bin:$GOPATH:$PATH

生效,并验证是否能够现实版本

canyue@workstation-CanYue:~/Desktop$ source /etc/profile
canyue@workstation-CanYue:~/Desktop$ go version
go version go1.17 linux/amd64

安装VSCode插件

解决插件项目TimeOutwent

由于默认源可能无法访问 也就是:proxy.golang.org

使用我们要使用: goproxy.cn

# 控制台执行
go env -w GOPROXY=https://goproxy.cn

修改GO命令行module功能的启用

什么作用先别管,我后面会说

# 控制台执行
go env -w GO111MODULE=auto

Hello World

使用VSCode创建项目

创建空文件夹并使用vsc打开

创建hello.go

点击右下键Install All

直到你看到它

All tools successfully installed. You are ready to Go. :)

至此,所有的工具包都安装成功了

编写代码

打开hello.go,并编辑它

/*定义包名,表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。*/
package main

/*导包,fmt 包实现了格式化 IO(输入/输出)的函数*/
import "fmt"

/*定义函数,main与Java Scala一致,是程序的入口,如果每个名为Init的函数,则最先执行它*/
func main() {     /*{不能单独在一行上*/
    fmt.Println("Hello World!!!") /*与Scala类似*/
}

如何跑起来

1.使用VSCode

可以使用快捷键 Ctrl + F5 以非调试模式运行

也可以在这里找到 【运行】 - 【以非调试模式运行】

2.使用命令行编译后运行

在hello.go文件所在目录下执行

go build hello.go

这将生成一个可执行文件,Windows就是.exe,Linux就是一个无后缀的二进制文件

一些理论

保留字

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

自带标识符

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

GO语言中,变量,函数,类等等程序实体不允许与自带标识符同名

数据类型

部分来源菜鸟教程https://m.runoob.com/go/go-data-types.html

序号

类型和描述

1

布尔型 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。

2

数字类型 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。

3

字符串类型: 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。

4

派生类型: 包括: (a) 指针类型(Pointer)(b) 数组类型 (c) 结构化类型(struct) (d) Channel 类型 (e) 函数类型 (f) 切片类型 (g) 接口类型(interface) (h) Map 类型

数字类型

Go 也有基于架构的类型,例如:int、uint 和 uintptr。

序号

类型和描述

1

uint8 无符号 8 位整型 (0 到 255)

2

uint16 无符号 16 位整型 (0 到 65535)

3

uint32 无符号 32 位整型 (0 到 4294967295)

4

uint64 无符号 64 位整型 (0 到 18446744073709551615)

5

int8 有符号 8 位整型 (-128 到 127)

6

int16 有符号 16 位整型 (-32768 到 32767)

7

int32 有符号 32 位整型 (-2147483648 到 2147483647)

8

int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

浮点型

序号

类型和描述

1

float32 IEEE-754 32位浮点型数

2

float64 IEEE-754 64位浮点型数

3

complex64 32 位实数和虚数

4

complex128 64 位实数和虚数


其他数字类型

以下列出了其他更多的数字类型:

序号

类型和描述

1

byte 类似 uint8

2

rune 类似 int32

3

uint 32 或 64 位

4

int 与 uint 一样大小

5

uintptr 无符号整型,用于存放一个指针

字符

类型

字节

描述

byte/utf8

1

单个UTF-8字符

rune

4

单个unicode字符

其他

  • GO语言大小写敏感

  • 在G语言中,给程序实体命名,不得使用@、$、%以及自带标识符与保留字

  • 函数实体命名第一个字符必须是字母或_,不能是数字

  • 只有package 名为 main 的包才被允许拥有main方法,一个可执行程序有且只有一个main包

  • import 只能导入非main包

  • GO语言无需像C或Java必须以;结尾

语法

注释

// 单行注释
/*
 多行注释
 123
 456
 */

变量

声明一个变量

var a float32 = 12 + 13  //var 变量名 类型 [= 表达式]  或 var 变量名 = 表达式   (将自动推断类型)

批量声明

var (
    a int = 12
    b string
    c float32
    d float64
    e [5]int //数组,后面说
)

//还可以

var a,b,c = 10,20,"abc"
var e,f,g int

简短的声明方法(又叫初始化声明)

a := 10 + 13  //将被自动推断类型,只能用于函数体内(局部变量)

为什么叫初始化声明,因为如果该变量被声明过,则会抛出异常

var a = 0
a := 10
no new variables on left side of :=

局部变量必须被使用

func main() {
    var a = 10
}
a declared but not used

匿名变量

有的函数会返回多个值

func test() (int,int){
    return 1,2
}

在GO中,你可以使用匿名变量接受它,然后不使用,匿名函数是不消耗内存的,这样就相当于丢弃了其中一个返回值,而不需要消耗额外的资源

a, _ := test()   //丢弃1
_, b := test()   //丢弃2

GO语言的多重赋值

之前要交换a,b值我们要使用中间变量

var a = 10
var b = 20
var tmp = a
a = b
b = tmp

或是使用算法

var a = 10
var b = 20
a = a ^ b
b = b ^ a
a = a ^ b

在GO语言,你这些都不要,你只需要

var a = 10
var b = 20
a, b = b, a

常量

声明(联系变量)

const a = 10
const b int64
const c,d,e = 10,20,"abc"
const (
    a = 10
    b int    
)

常量的枚举

func main() {
    const (
        a = 10
        b = "c会学我"
        c
    )
    fmt.Println(a, b, c)
}
10 c会学我 c会学我

以这种方式定义的常量,若无初始值和类型,则会与上一个非空的常量值相同

iota常量

iota常量初始值为了,每出现一个const常量,iota常量会被编译器自增1(但程序无法修改它)

func main() {
    const (
        a = iota
        b = iota
        c = "不一定要赋值才会自增"
        d = iota
    )
    fmt.Println(a, b, c, d)
}
0 1 不一定要赋值才会自增 3

iota只能在常量声明时出现,不能直接用,例如放在Println方法内

类型别名与定义新的类型定义

取一个类型别名

别名就像一个外号,都是同一个人

type String = string
    var a string = "aa"
    var b String = "bb"

定义个新类

再来一个新的一个人,只不过这两个一样

type String string
    var a string = "aa"
    var b String = "bb"

在Go 1.9之前,没有别名,定义新类的方法是 type 新类型名 = 类型名

与现在取别名一致

类型别名在编译后将去除,因为别名与原名是同一个东西,GO只会保留原先的

别名主要作用还是用于解决程序兼容性的问题

运算符

恕我 + - * / == >= <= != < > | || & && !>> << 忽略 这些与其他语言一致,将写不常见的

若 a 1;b 2

运算符

描述

实例

++

自增

a ++ 结果 2

--

自减

a -- 结果 0

+=

相加后赋值

-=

相减后赋值

*=

相乘后赋值

/=

相除后赋值

<<= \

>>=

位移后赋值

&=

按位与后赋值

^=

按位异或后赋值

! = (连起来)

按位或后赋值

&

也可是输出存储地址

&a

*

也可是表指针变量

*a

优先级(由上往下,从大到小)

运算符

^ !

/ * % << >> & &^

+ - \

== != <<= >>= >= <= 以及其他xx后赋值

<-

&&

\

控制流程

IF语句(不多赘述)

func main() {
    if false {
        if true {
            /*代码*/
        }
    } else if 1+1 == 3 {
        fmt.Println("不可能,绝对不可能")
    } else {
        /*代码*/
    }
}

SWITCH语句(不多赘述)

const score = 90
var msg string
switch { //相当于  switch true  你也可以使用switch xxx csae 1 这种
case score >= 90:
    msg = "优秀"
case score >= 75 && score < 90:
    msg = "良好"
case score >= 60 && score < 75:
    msg = "及格"
case score < 60:
    msg = "不及格"
default: //都不满足
    msg = "无数据"
}

SELECT语句

类似SWITCH语句,不一样的是SELECT会随机执行一个满足条件的case,如果一个都没有,就会将其堵塞,直到有满足条件的

func main() {
    ch1 := make(chan int)
    ch2 := make(chan string)

    go func() {
        ch1 <- 100
    }()

    go func() {
        num2 := <-ch2
        fmt.Println(num2)
    }()

    select {
    case num1 := <-ch1: //读取channel数据
        fmt.Println("ch1中的数据是:", num1)
    case ch2 <- "201": //channel写入数据
        fmt.Println("ch2有数据写入")
    }
}
201
ch2有数据写入

循环

go语言没有while循环

for循环

语法类似JS

func main() {
    for i := 1; i <= 9; i++ {
        for j := 1; j <= i; j++ {
            fmt.Printf(" %d * %d = %d", j, i, j*i)
        }
        fmt.Println()
    }
}
 1 * 1 = 1
 1 * 2 = 2 2 * 2 = 4
 1 * 3 = 3 2 * 3 = 6 3 * 3 = 9
 1 * 4 = 4 2 * 4 = 8 3 * 4 = 12 4 * 4 = 16
 1 * 5 = 5 2 * 5 = 10 3 * 5 = 15 4 * 5 = 20 5 * 5 = 25
 1 * 6 = 6 2 * 6 = 12 3 * 6 = 18 4 * 6 = 24 5 * 6 = 30 6 * 6 = 36
 1 * 7 = 7 2 * 7 = 14 3 * 7 = 21 4 * 7 = 28 5 * 7 = 35 6 * 7 = 42 7 * 7 = 49
 1 * 8 = 8 2 * 8 = 16 3 * 8 = 24 4 * 8 = 32 5 * 8 = 40 6 * 8 = 48 7 * 8 = 56 8 * 8 = 64
 1 * 9 = 9 2 * 9 = 18 3 * 9 = 27 4 * 9 = 36 5 * 9 = 45 6 * 9 = 54 7 * 9 = 63 8 * 9 = 72 9 * 9 = 81

遍历我后面讲

循环控制语句

break语句

结束循环(不多赘述)

continue

跳过这圈循环(不多赘述)

goto

无条件跳转到某一标签所在行

(CMD有类似的写法)

func main() {
    var num = 0
A:   //标签
    fmt.Println("位置A")
    if num == 3 {
        goto B   //无条件跳转到标签A所在行
    }
    num++
    goto A
B:
    fmt.Println("跳转到B")
}
位置A
位置A
位置A
位置A
跳转到B

函数

定义函数

//func 函数名 (参数列表) (返回值列表) {}    || 若相邻参数数据类型一致,只需在最后一个指定类型即可
func myFunc (a,b int, c ... string) (int,string){    //c为不定长参数
    return a + b , ""    //不像Python返回一个列表,这是真真切切的两个值
}

其实在GO语言中,函数也是一种类型,可以被保存在变量中(有点像Scala,只不过Scala函数不是类型)

返回值列表不仅仅可以放返回数据类型,还可以带上名称

这时候返回值就是一个局部变量,最后只需一个return就可以自动将变量值返回

func main() {
    fmt.Println(mySum(3, 4))
}

func mySum(num1 int, num2 int) (sum int) {
    sum = num1 + num2
    return
}
7

作用域

函数内定义的变量叫局部变量,局部变量仅作用与该函数题内,生命周期与该函数相同

函数外定义的叫全局变量,全局变量作用与整个包甚至外部包吗,所以之前与main函数相同

GO允许局部变量与某一全局变量同名,在局部变量所在的函数体内,局部变量优先级高于全局变量

意思作用于对常量,类型,函数皆适用

匿名函数

func main() {
    f := func(msg string) {
        fmt.Println(msg)
    }
    f("Hello World")
}
Hello World

也可以这样用

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

个人觉得没有Scala灵活

闭包

闭包严格意义上不算是函数,函数内部可执行的代码在函数定义时就被实现,且不会随程序的运行额发生改变,且一个函数通常只有一个实例

闭包在运行是可以有多个实例,不同的引用环境会生产多个实例

闭包在有些变成语言里面叫lambda表达式(熟悉了吧)

函数本身不具有记忆性,而与环境结合的闭包才具有记忆性(下面有实验)

函数是编译器静态的概念,闭包是运行期间的动态的概念

也不是所有的变成语言都支持闭包,闭包需依赖语言的以下特性

  • 函数是一等公民,及函数可以作为另一个函数的参数或返回值(高阶函数的概念),还可以作为一个变量的值

  • 函数之间可以嵌套定义

  • 支持匿名函数

  • 可以捕获运行时上下文环境,并支持吧函数和环境组合成一个可调用的实体

对比实验(记忆性)

无闭包

func main() {
    for i := 1; i <= 5; i++ {
        fmt.Println(myAdd(i))
    }
}

func myAdd(num int) int {
    sum := 0
    sum += num
    return sum
}
1
2
3
4
5

闭包

func main() {
    f := myAdd()
    for i := 1; i <= 5; i++ {
        fmt.Println(f(i)) //每个小循环又是一个实例
    }
}

func myAdd() func(int) int { //分开看  返回一个函数
    sum := 0
    return func(num int) int { //返回一个匿名函数
        sum += num
        return sum
    }
}
1
3
6
10
15

可变参数

可变参数及可接受不确定的参数数量,当一个函数参数数量不确定,但确定参数的数据类型时,就可以使用可变参数

可变参数在GO中使用 ... 表示

可变参数必须写在最后,且一个函数只能有一个可变参数

func main() {
    sum := mySum(10, 12, 15)
    fmt.Println(sum)
}

func mySum(num ...int) (sum int) {
    for _, n := range num { //for index,value := range num  遍历我后面讲
        sum += n
    }
    return
}
37

递归

递归,就是函数内部调用函数本身

func main() {
    fmt.Println(factorial(3))
}

func factorial(n int) (int) {
    if n == 0 {
        return 1
    }
    return n * factorial(n-1)
}
6

指针

一个保存了 另一个变量 的 内存地址 的 变量

声明指针

与声明变量极为类似

var a *int   //在数据类型前加*,数据类型是被指向变量的数据类型

如何给指针赋值

还记得前面提到的运算符中&符号还可以表示返回某个元素的内存地址吗??

var a *int   //在数据类型前加*,数据类型是被指向变量的数据类型
var num = 12
a = &num
//结合以往知识点,指针还可以这样声明(作用域:局部)
a1 := &num

空指针

但一个指针被声明但未被赋值,就是空指针

空指针值为nil

使用指针

我前面也提到过,*指针名可以表示指针指向的变量

func main() {
    num := 10
    a := &num
    *a = 12   //*指针名表示指针指向的变量,间接修改了变量
    fmt.Println(*a)   //指针也可作为参数
}
12

指针的指针

一个指针指向一个指向元素的指针

在使用的时候需要多个*

例如

func main() {
    num := 10
    a := &num
    b := &a
    **b = 12 //分开看 * *b | **b ==> *a ==> num
    fmt.Println(**b)
}
12

指针的指针的指针同理,理解就行,我就不套娃了

浅拷贝与深拷贝

指针赋值给别人叫拷贝

浅拷贝只复制了一份指向自己内存地址的指针,所以在修改副本的时候会影响真身

类型赋值给别人叫拷贝

深拷贝值将内容复制给了一个新的内存位置,所以在修改副本时不改变真身

容器(集合)

数组

语法

var 数组名 [数组长度] 数据类型 {[初始元素]}    //一维简化
var 数组名 [维度1长度][ [维度2长度] ... ] 数据类型 { {[维度1初始元素]} , [{[维度2初始元素]}] ... }

数组长度的方括号不可省略

若不指定数据长度,可省略不写,GO将按照初始元素的数量指定数组长度,这种声明方法在Go中称为切片

可以将数组长度写成...效果同上,只不过这次声明的是数组

长度

长度使用只带len()函数获取

func main() {
    a := []int{1, 2, 3}
    fmt.Println(len(a))
}
3

数组中 长度与容量相同,使用GO语言有个cap()函数,在数组中,与len()结果相同,具体在下方遍历章节有实验

访问

可以使用[index]访问

func main() {
    a := []int{1, 2, 3}
    fmt.Println(a[1])
    b := [2][3]int{{1, 3}, {5, 7, 9}}
    fmt.Println(b[1][1])
}
2
7

切片

GO中,通过声明一个未指定长度的数组来定义切片

虽然不定义长度,但容量还是有限

GO不支持负数下标

func main() {
    a := []int{1, 2, 3, 4, 5}
    fmt.Println(a[:])   //所有元素
    fmt.Println(a[1:])  //下标为1到最后一个元素
    fmt.Println(a[:3])  //起始元素到下标为3的元素(不包含)
    fmt.Println(a[1:3]) //下标为1到下标为3的元素(不包含)
}
[1 2 3 4 5]
[2 3 4 5]
[1 2 3]
[2 3]

返回的切片不是数组,它只是数组的一种引用,对切片的操作会映射到原数组上

遍历

func main() {
    a := []int{1, 2, 3}
    //方法1
    for i := 0; i < len(a); i++ {
        fmt.Print(a[i], " ")
    }
    fmt.Println("\n---------")
    //方法2
    //indexd 或 value 可使用匿名变量丢弃 常写作 for _, value := range xxx
    for index, value := range a {
        fmt.Print("i:", index, ";v:", value, " ")
    }
    fmt.Println("\n---------")

    //多维(上面两种方法都行)
    c := [3][3]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8}, //需要,   //这里我故意留一个空,使得数组的一个元素每个初始值
    }
    for _, v1 := range c {
        for _, v2 := range v1 {
            fmt.Print(v2, " ")
        }
    }
}
1 2 3 
---------
i:0;v:1 i:1;v:2 i:2;v:3 
---------
1 2 3 4 5 6 7 8 0     //使用int类型的默认值0填充

该语法同样适用于其他集合,包括字符串

修改数组

添加元素

append()方法会返回一个在原切片的基础上追加了元素的切片

使用append()方法向切片末尾追加一个或多个元素

这里为什么是切片,结合之前的实验,以及数组特性,数组是定长的,数组空值会自动按数据类型默认值补齐,所以,理论上无法追加!!!

func main() {
    a := []int{1, 2, 3, 4, 5}
    a = append(a, 6)           //追加元素
    a = append(a, 7, 8, 9, 10) //追加多个元素
    fmt.Println(a)
}
[1 2 3 4 5 6 7 8 9 10]

删除元素

这里吐槽GO,切片的删除没有提供方法实现。切片元素的删除我分为两类

  1. 掐头去尾

删除头尾可以直接使用下标进行切片操作

func main() {
    a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    a = a[2:]           //去前两个
    a = a[:len(a)-2]    //去后两个
    a = a[2 : len(a)-2] //头尾都去两个
    fmt.Println(a)
}
[5]

  1. 去任意某个

这就有点特殊,在此之前,先拓展一个方法 ...,语法:

数组或切片...

...在GO语言中的地位相当于Scala的_,是该语言的语法糖

在GO语言中,...有两个意思,一为不定长,这在函数定义上提到过。二为打散某个集合再传递

GO语言无法直接拼接两个切片,append()方法添加多个元素必须传递在其不定长参数上,怎么办,只能打散再传递。

func main() {
    a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    a = append(a[:5], a[5+1:]...) //去下标5
    a = append(a[:2], a[4+1:]...) //再去下标234
    fmt.Println(a)
}
[1 2 7 8 9]

事实证明,GO语言没Scala好用

复制元素

copy()方法可以将一个切片的元素复制到另一个切片,由于上面提到的原因,所以数组还是不能使用copy()方法

copy()方法复制元素遵循如下规则

  • 复制的元素会放在原来的位置(1,2,3 替换了11,12,13)

  • 要是被复制到的切片容量不足,那就只会复制能装得下的前几个(只复制1,2,3)

func main() {
    a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    b := []int{11, 12, 13}
    count := copy(b, a)    //coty方法将返回复制的元素数量   //注意方向,这个写法是a->b
    fmt.Println(a)
    fmt.Println(b)
    fmt.Println(count)
}
[1 2 3 4 5 6 7 8 9]
[1 2 3 4 5 6 7 8 9 4 4 4]
9

MAP

哈希表,键值对,数据结构我不多叙述

语法

var 变量名 = map [key类型] value类型 {[k1:v1, k2:v2 ....]}              //不初始化,值为nil
或
var 变量名 = make (map [key类型] value类型)                             //不初始化,值非nil

[key类型] 不代表可省略,而是key类型需放在[]中

赋值

func main() {
    a := map[string]int{
        "李二狗": 12,
        "张三猫": 13,     //不可省略,
    }
    a["李四牛"] = 15
    fmt.Println(a)
}
map[张三猫:13 李二狗:12 李四牛:15]

删除

删除可以使用delete()方法

func main() {
    a := map[string]int{
        "李二狗": 12,
        "张三猫": 13, //不可省略,
    }
    a["李四牛"] = 15
    delete(a, "李四牛") //只能一个个来
    fmt.Println(a)
}
map[张三猫:13 李二狗:12]

查询

func main() {
    a := map[string]int{
        "李二狗": 12,
        "张三猫": 13, //不可省略,
    }
    a["李四牛"] = 15
    fmt.Println(a["李四牛"])
    fmt.Println(a["王五虎"])
}

查不到将返回该值类型的默认值

15
0

遍历

func main() {
    a := map[string]int{
        "李二狗": 12,
        "张三猫": 13,      //不可省略,
    }
    a["李四牛"] = 15
    for k, v := range a { //v可省略
        fmt.Println(k, v)
    }
}
李二狗 12
张三猫 13
李四牛 15

常用包

详见https://studygolang.com/pkgdoc

来源https://github.com/polaris1119/pkgdoc/

面向对象

准确来说,Go语言没有类的概念,不过却有struct(结构体)和interface(接口)

结构体(Scala样例类阉割版)

单一数据类型已无法满足现实的开发需求,于是GO语言提供了结构体来单一复杂的数据类型

结构体由相同或不同类型的数据结构组成

使用结构体需注意

  • 结构体的名称在一个包类不能重复

  • 结构体的属性,也叫字段,在一个结构体类是唯一的

  • 在定义结构体时,同类型的结构体可以写同一行

  • 结构体在被实例化后才会被分配内存

  • 结构体是一个值类型,而不是指针,将结构体作为参数传递到函数中,在函数中进行修改,不影响结构体本身

  • 结构体可以作为参数以及返回值,其指针也一样

语法

type Cat struct { //定义结构体
    name, color string
    age         int
}

func main() {
    //实例化方法1:
    var huahua = Cat{name: "花花", color: "三花", age: 5} //不一定要都指定,这和Scala不同
    fmt.Println(huahua.name)                          //调用参数
    //实例化方法2:后面再给参数赋值
    var mimi Cat //注意没有等号   等同于 mimi := Cat{}
    mimi.name = "咪咪"
    fmt.Println(mimi.name)
    fmt.Println(mimi.age) //为赋值的参数值为该类型的初始值
}
花花
咪咪
0

匿名结构体与匿名字段

func main() {
    res := struct {     //无需type
        a int           //参数
        float32            //匿名参数(会使用类型名作为参数名,一个结构体内一个类型只能有一个匿名参数)
    }{12,0.12}          //初始化参数值
}

嵌套(聚合和继承)

Go貌似没有传统意义上的继承

不过我们可以使用嵌套实现,在一个结构体内嵌套一个结构体,被嵌套的结构体与外层结构体的关系:

  • 聚合关系:一个结构体是另一个结构体的属性

  • 继承关系:被嵌套结构体与外层结构体是是父子关系

GO语言其实没有继承,所以无法实现其他语言的许多操作

例如不能给蓝猫结构体某个参数初始值

//父类
type Cat struct {
    name string
    age  int
}

//子类
type BlueCat struct {
    Cat   //匿名就行
    color string
}

func main() {
    //实例1
    tom := BlueCat{Cat{"汤姆", 5}, "蓝"}
    //实例2
    blue := BlueCat{}
    blue.name = "布鲁"
    blue.age = 6
    blue.color = "蓝"
}

方法

方法本质上是函数,只不过方法与函数在逻辑上不同

函数属于包,被包调用;方法属于某一结构体,通过结构体变量类调用

方法在定义的时候语法与函数类似,不过要指定属于那个结构体

语法

func (变量名 结构体名) 方法名 (参数列表) 返回值列表 {
    方法体
}

GO语言不支持类,方法是为了实现一些类似于类的功能

同名的方法可以在多个结构体上定义,所以以函数不同,方法是可以重名的

样例

type Cat struct {
    name string
    age  int
}

//实现一个方法,这时候相当于有个Cat类,类里面有个方法叫printInFO()
func (cat Cat) printInFo() {
    fmt.Printf("%s几年%d岁了", cat.name, cat.age)
}

func main() {
    //实例化一个结构体
    tom := Cat{"Tom", 5}
    tom.printInFo()
}
Tom几年5岁了

继承

方法会随着结构体的'继承'而被继承

即其中一匿名字段实现了一个方法,那这个方法就会被继承到该结构体

type Cat struct {
    name string
    age  int
}

//“继承”
type BlueCat struct {
    Cat
}

//实现一个方法,这时候相当于有个Cat类,类里面有个方法叫printInFO()
func (cat Cat) printInFo() {
    fmt.Printf("%s几年%d岁了", cat.name, cat.age)
}

func main() {
    //实例化一个结构体
    tom := BlueCat{Cat{"Tom", 6}}
    tom.printInFo()
}
Tom几年6岁了

重写

若一结构体内的一匿名字段实现了一个方法,但结构体也实现了一个同名方法,那这方法就会被重写

type Cat struct {
    name string
    age  int
}

//“继承”
type BlueCat struct {
    Cat
}

//实现一个方法,这时候相当于有个Cat类,类里面有个方法叫printInFO()
func (cat Cat) printInFo() {
    fmt.Printf("%s几年%d岁了", cat.name, cat.age)
}

//BlueCat再实现一个同名方法
func (cat BlueCat) printInFo() {
    fmt.Printf("%s几年%d岁了,它是一中蓝色的猫", cat.name, cat.age)
}

func main() {
    //实例化一个结构体
    tom := BlueCat{Cat{"Tom", 6}}
    tom.printInFo()
}
Tom几年6岁了,它是一中蓝色的猫

接口

在GO语言中,任何定义了接口中所有方法的结构体就自动隐式实现了接口

语法

type 接口名 interface {
    方法1([参数列表]) [返回值]
    方法2([参数列表]) [返回值]
    .....
}

样例

type Cat interface {
    printInFo()
}

//“继承”
type BlueCat struct {
    name string
    age  int
}

//BlueCat再实现一个同名方法
func (cat BlueCat) printInFo() {
    fmt.Printf("%s几年%d岁了,它是一中蓝色的猫", cat.name, cat.age)
}

func main() {
    //实例化一个结构体
    tom := BlueCat{"Tom", 7}
    tom.printInFo()
}

空接口

空接口中没有任何方法,任何结构体都可以实现该接口,空接口类似以于Java以及Scala的object

异常

error

error错误,程序出现了不正常的情况,一般用于自定义一个函数或方法可能发生的异常,将其返回

error在GO语言中本质是一个接口,其中包含Error()方法,错误值存储在变量中,最后通过该发放返回,返回的错误必须是某个函数或方法的最后一个返回值

在GO语言中,通常判断函数或方法是否发生了错误,可以对比该方法或函数返回值的最后一个值是不是nil,如果是,则未发生错误

import (
    "errors"
    "fmt"
)

func myMath(num1 float32, num2 float32) (float32, error) {
    if num2 == 0 {
        return 0, errors.New("除数为0")
    } else {
        return num1 / num2, nil
    }
}

func main() {
    res, err := myMath(1, 0)
    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(res)
    }
}

panic

恐慌,程序出现了无法处理的灾难级的错误,通常程序在遇到无法处理的问题且没被recover拦截,将抛出painc

panic()是一个内建函数,当程序运行到这时,程序将被中断运行

与error不同,panic通常用于报告致命错误,例如在数组越界时

func myFunc(n int) { //放在函数,骗过编译器
    a := [3]int{}
    a[n] = 12
}

func main() {
    myFunc(10)
}

GO语言就会抛出一个panic而不是error

panic: runtime error: index out of range [10] with length 3

goroutine 1 [running]:
main.myFunc(0xa)
    /home/canyue/Desktop/GOProject/hello.go:6 +0x52
main.main()
    /home/canyue/Desktop/GOProject/hello.go:10 +0x1e

对比error返回的消息,panic抛出了错误将更为详细,那是因为panic的工作流程有关

当一行代码发生panic,GO会立即停止该行代码,然后将控制权转移到所在的函数或方法,然后该函数或方法内的代码将立即停止,控制权立即转移到其调用者以及上级调用者,直到最初调用。在此期间,panic会搜集详细消息,最终抛出

panic本质是一个内建函数,使用我们其实可以随时调用panic使得一个程序陷入恐慌,然后停止运行

func myFunc(n int) { //放在函数,骗过编译器
    fmt.Println("hello")
    panic("手动出发恐慌")
    fmt.Println("程序不在执行到此") //VSC提示   unreachable code  遥不可及的,不会执行到这
}

func main() {
    myFunc(10)
}
DAP server listening at: 127.0.0.1:36413
hello
panic: 手动出发恐慌

goroutine 1 [running]:
main.myFunc(0xa)
    /home/canyue/Desktop/GOProject/hello.go:8 +0x7e
main.main()
    /home/canyue/Desktop/GOProject/hello.go:13 +0x1e

defer

defer用于延迟一个函数或方法,具体为如果一各函数有多个defer语句,GO会先执行没有带defer分语句,直到最后运行到最后一个defer语句时,再逆序执行所有带defer的语句

func myFunc() {
    defer fmt.Println("1")
    fmt.Println("2")
    defer fmt.Println("3")
    defer fmt.Println("4")
}

func main() {
    myFunc()
}
2
4
3
1

虽然defer语句是运行到最后一句在一块运行的,不过,如果defer语句中有传参,那参数则是事先在到达该句时先赋值

func myFunc(num int) {
    println(num)
}

func main() {
    n := 1
    defer myFunc(n)
    n++
    myFunc(n)
    n++
    defer myFunc(n)
}
2
3
1

异常处理

GO语言与Java、Scala的异常处理上有很大的不同

因为GO语言的作者在觉得,将异常控制流程混在一起(try...catch...finally)会使代码变得混乱

在上面提到了程序在发生panic时会导致程序恐慌奔溃,其实我们可以拦截panic,具体如下

recover

recover()本质是一个内建函数,在执行它时,会捕获panic的异常消息,并让程序正常运行(出现panic的函数会方法除外),如果程序正常运行,则返回nil

不过要注意,recover只能在defer延迟语句内执行才会生效,且放在可能会抛出panic的代码执行顺序之后

func myFunc() { //放在函数,骗过编译器
    defer func() { //defer 用于延迟一个函数或方法,通常这会写一个匿名函数
        msg := recover()
        if msg != nil {
            fmt.Printf("发生paincm,消息:%s", msg)
        }
    }()
    /*除数5到0,0会抛出异常*/
    for i := 5; i >= -5; i-- {
        fmt.Println(10 / i)
    }
}

func main() {
    myFunc()
}
2
2
3
5
10
发生paincm,消息:runtime error: integer divide by zero

导致panic的函数或方法将不会继续执行

IO

键盘输入

类似Python的input

只需向Scanln()方法传递若干个指针,即可实现

func main() {
    name := ""
    age := 0
    fmt.Scanln(&name, &age)
    fmt.Printf("%s几年%d岁了\n", name, age)
}
canyue@workstation-CanYue:~/Desktop/GOProject$ ./hello
小明 12
小明几年12岁了

每个输入之间使用[空格]隔开

VSC的非调试运行会出问题

文件系统

在文件系统上的IO操作

查询文件信息

import (
    "fmt"
    "os"
)

func main() {
    //字符串表示路径
    path := "./hello.sol" //支持相对、绝对路径
    //Go语言中,文件或目录使用fileStat类型表示
    stat, error := os.Stat(path) //通过os包中的stat返回两个数据,一是文件的消息,二是错误(前面提到过)
    if error == nil {
        fmt.Printf("数据类型: %T \n", stat)
        fmt.Printf("文件名:%s \n", stat.Name())
        fmt.Printf("是否是一个目录:%t \n", stat.IsDir())
        fmt.Printf("文件权限:%s \n", stat.Mode())
        fmt.Printf("文件最后修改时间:%s \n", stat.ModTime())
        fmt.Printf("文件大小:%d bit\n", stat.Size())
    }
}
数据类型: *os.fileStat 
文件名:hello.sol 
是否是一个目录:false 
文件权限:-rw-r--r-- 
文件最后修改时间:2022-08-19 14:28:06.674944276 +0800 CST 
文件大小:167 bit

创建与删除

func main() {
    //创建
    err := os.Mkdir("./test", os.ModeDir) //创建文件夹
    if err == nil {
        fmt.Println("文件夹创建成功")
        //递归生成文件夹
        err2 := os.MkdirAll("canyue/go/test", os.ModePerm)
        if err2 == nil {
            fmt.Println("递归生成成功")
        } //递归删除不会因为已存在而抛出异常
        //创建文件
        file, err3 := os.Create("./test/canyue.txt") //返回该文件以及错误,已存在文件将覆盖
        if err3 == nil {
            fmt.Printf("文件创建成功,内存地址:%v", file)
        }
    } else {
        fmt.Println("文件夹已存在") //已存在将抛出错误
    }
    //删除
    os.Remove("./test/canyue.txt")
    os.Remove("./test") //只能删除一个空目录
    //递归删除
    os.RemoveAll("./canyue")
}

文件读写

OS包实现

文件打开方式: os.关键字

关键字

模式

O_RDONLY

只读(read-only)

O_WRONLY

只写(write-only)

O_RDWR

可读可写(read-write)

O_APPEND

追加写

O_CREATE

文件不存在时创建该文件

打开方式之间可以使用 | 分隔,代表同时使用多个模式

通常与O_CREATE同时使用

func main() {
    file, err := os.OpenFile("./canyue.txt", os.O_RDWR|os.O_CREATE, os.ModePerm) //打开文件
    if err == nil {
        defer file.Close() //关闭文件,通常使用defer语句
        fmt.Println("文件打开成功")
        //写入文件
        n, err2 := file.Write([]byte("你好\n我是残月"))
        if err2 == nil {
            fmt.Println("文件写入成功,写入字节", n)
        }
    } else {
        fmt.Println("文件打开错误")
    }
}
func main() {
    //OS包实现
    file, err := os.OpenFile("./canyue.txt", os.O_RDWR|os.O_CREATE, os.ModePerm) //打开文件
    if err == nil {
        //读取文件
        buf := make([]byte, 512) //创建一个512字节的缓冲区
        for {                    //一次读一行
            n, err3 := file.Read(buf) //字节数,错误
            if n == 0 || err3 != nil {
                fmt.Println("打印完成")
                break
            }
            fmt.Println(string(buf[:n])) //将缓冲区所有数据装换为字符串
        }
    } else {
        fmt.Println("文件打开错误")
    }
}
你好
我是残月
打印完成

ioutil包实现

func main() {
    //打开刚刚的文件
    data1, err1 := ioutil.ReadFile("./canyue.txt")
    if err1 == nil {
        fmt.Println(string(data1), "\n------------")
    } else {
        fmt.Println("读取canyue.txt失败", err1)
    }
    //无法读取不存在的文件
    data2, err2 := ioutil.ReadFile("./canyue.md")
    if err2 == nil {
        fmt.Println(string(data2), "\n------------")
    } else {
        fmt.Println("读取canyue.md失败", err2)
    }
    //写文件 (追加写入,不存在将会创建文件)
    writeText01 := []byte("文件001") //传入的是字节类型的数组
    err := ioutil.WriteFile("./test/001.txt", writeText01, os.ModePerm) == nil
    writeText02 := []byte("文件002")
    err = ioutil.WriteFile("./test/002.txt", writeText02, os.ModePerm) == nil && err
    writeText03 := []byte("文件003")
    err = ioutil.WriteFile("./test/003.txt", writeText03, os.ModePerm) == nil && err
    if err {
        fmt.Println("文件写入成功")
    } else {
        fmt.Println("有文件写入失败")
    }

}
你好
我是残月 
------------
读取canyue.md失败 open ./canyue.md: no such file or directory
文件写入成功

原生网络编程

常见HTTP代码

https://http.cat/

RESTful规范

https://blog.csdn.net/qq_26460841/article/details/119977022

一个Http测试网站

http://httpbin.org

JSON字符串转Map

func main() {
    //原数据
    originData := `[{"name":"小明","age":"12","sex":"男"}]` //`` 不转译字符串
    //解析后的数据
    var data []map[string]string
    //解析
    json.Unmarshal([]byte(originData), &data)
    fmt.Println(data)
}
[map[age:12 name:小明 sex:男]]

HTTP客户端

GO语言原生内置HTTP包net/http

包内提供了最简介的HTTP客户端实现方式

简单演示

func main() {
    httpRequest("GET", "http://httpbin.org/get")
}

//http请求
func httpRequest(method string, url string) {
    //创建客户端
    client := http.Client{}
    //创建请求
    request, err := http.NewRequest(method, url, nil)
    checkError(err)
    //添加Cookie
    cookieName := &http.Cookie{Name: "n1", Value: "v1"}
    request.AddCookie(cookieName)
    //设置请求头
    request.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0")
    //发送Do请求
    response, err := client.Do(request)
    checkError(err)
    defer response.Body.Close()
    //查看请求头数据
    fmt.Println("Header:", request.Header)
    //查看状态
    fmt.Println("HTTPCode:", response.StatusCode)
    fmt.Println("HTTPState:", response.Status)
    //操作数据
    if response.StatusCode == 200 {
        data, err := ioutil.ReadAll(response.Body)
        checkError(err)
        fmt.Println(string(data))
    } else {
        fmt.Println("访问异常:", response.Status)
    }
}

//检查错误
func checkError(err error) {
    if err != nil {
        panic(err)
    }
}
Header: map[Cookie:[n1=v1] User-Agent:[Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0]]
HTTPCode: 200
HTTPState: 200 OK
{
  "args": {}, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Cookie": "n1=v1", 
    "Host": "httpbin.org", 
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0", 
    "X-Amzn-Trace-Id": "Root=1-630478c3-3b53d2801c6f0b190dab7480"
  }, 
  "origin": "xxx.xxx.xxx.xxx", 
  "url": "http://httpbin.org/get"
}

Get请求

client.Get()

func main() {
    httpRequest("http://httpbin.org/get?param1=12&param2=13")
}

//http请求
func httpRequest(url string) {
    //创建客户端
    client := http.Client{}
    //发送请求
    request, err := client.Get(url)
    checkError(err)
    defer request.Body.Close()
    //处理返回
    if request.StatusCode == 200 {
        data, err := ioutil.ReadAll(request.Body)
        checkError(err)
        fmt.Println(string(data))
    } else {
        fmt.Println(request.Status)
    }

}

//检查错误
func checkError(err error) {
    if err != nil {
        panic(err)
    }
}
{
  "args": {
    "param1": "12", 
    "param2": "13"
  }, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-63047a79-1bcdea4942031af9214acd15"
  }, 
  "origin": "xxx.xxx.xxx.xxx", 
  "url": "http://httpbin.org/get?param1=12,param2=13"
}

http.Get()

两者区别在,后者无需创建一个客户端,直接发送请求

func main() {
    httpRequest("http://httpbin.org/get?param1=12&param2=13")
}

//http请求
func httpRequest(url string) {
    request, err := http.Get(url)
    checkError(err)
    //处理返回
    if request.StatusCode == 200 {
        data, err := ioutil.ReadAll(request.Body)
        checkError(err)
        fmt.Println(string(data))
    } else {
        fmt.Println(request.Status)
    }

}

//检查错误
func checkError(err error) {
    if err != nil {
        panic(err)
    }
}
{
  "args": {
    "param1": "12", 
    "param2": "13"
  }, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-63047b82-70e59d7d44c5bfe337698d64"
  }, 
  "origin": "xxx.xxx.xxx.xxx", 
  "url": "http://httpbin.org/get?param1=12&param2=13"
}

Post请求

client.Post()

func main() {
    params := map[string]string{
        "name": "小明",
        "sex":  "男",
    }
    httpRequest("http://httpbin.org/post", params)
}

//http请求
func httpRequest(u string, params map[string]string) {
    //创建客户端
    client := http.Client{}
    //处理请求参数
    data := url.Values{}
    for k, v := range params {
        data.Add(k, v)
    }
    requestData := strings.NewReader(data.Encode())
    //发送请求
    request, err := client.Post(
        u,
        "application/x-www-form-urlencoded",
        requestData,
    )
    checkError(err)
    //处理返回
    if request.StatusCode == 200 {
        data, err := ioutil.ReadAll(request.Body)
        checkError(err)
        fmt.Println(string(data))
    } else {
        fmt.Println(request.Status)
    }
}
{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "name": "\u5c0f\u660e", 
    "sex": "\u7537"
  }, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Content-Length": "37", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-63047f1a-0523d8e45f53e8c311926ecd"
  }, 
  "json": null, 
  "origin": "xxx.xxx.xxx.xxx", 
  "url": "http://httpbin.org/post"
}

http.Post()

func main() {
    params := map[string]string{
        "name": "小明",
        "sex":  "男",
    }
    httpRequest("http://httpbin.org/post", params)
}

//http请求
func httpRequest(u string, params map[string]string) {
    //处理请求参数
    data := url.Values{}
    for k, v := range params {
        data.Add(k, v)
    }
    requestData := strings.NewReader(data.Encode())
    //发送请求
    request, err := http.Post(
        u,                                          //url
        "application/x-www-form-urlencoded",        //第二个参数必须设置成"application/x-www-form-urlencoded",否则post参数无法传递
        requestData,
    )
    checkError(err)
    //处理返回
    if request.StatusCode == 200 {
        data, err := ioutil.ReadAll(request.Body)
        checkError(err)
        fmt.Println(string(data))
    } else {
        fmt.Println(request.Status)
    }
}
{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "name": "\u5c0f\u660e", 
    "sex": "\u7537"
  }, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Content-Length": "37", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-63047f69-1d941f48748d9bf60bceacc7"
  }, 
  "json": null, 
  "origin": "xxx.xxx.xxx.xxx", 
  "url": "http://httpbin.org/post"
}

  • 0