简简单单,入门Golang...

【Go】入门Go语言

前言

Go这门语言在当下已经越来越火热了,无论是后端开发岗,还是逆向安全岗位,亦或是渗透领域,乃至脚本小子...各个领域的人员都在分Go这一杯羹。

并且在最近越来越多的CTF比赛中,Go逆向、Go pwn,甚至是misc中的底层使用go编写的传输协议,Go都大放异彩。

很早之前就给自己定下学习Go的目标,但是也是学一段时间摆一段时间,没有静下心来好好学一下。趁着这次寒假的机会,把Go入门一下!

1.Go环境配置

这一章节就不过多赘述,因为是记录个人学习过程,所以也不会花太多时间来解释比较基础的东西。

Go环境的配置就分为三步走:

  • 去官网下载(zip方式或者一键安装的方式都可以)
  • 配置相对应的环境变量(win和linux、mac的方式不同,但是殊途同归)
  • 下载自己喜欢的编辑器(我个人一直是使用VsCode)

2.Go Modules

在编写最简单的helloworld程序之前,使用go mod init xxx来生成一个go.mod文件,这个文件会记录Go的版本信息,需要导入的包等信息。

现在再来说明的概念

  • Go程序是由包构成的,程序从main包开始执行
  • import + 括号的形式是简写,可以每一行import一个包
  • 在包字符串前面可以给包起别名
package main

import (
	sout "fmt"	// 给fmt包起别名叫sout
)

func main() {
	sout.Println("114514")
}

2.1 单文件运行

初始化完了mod之后,可以进行最简单的编程,我这里创建了一个hello.go

package main

import "fmt"

func sayHelloWorld() {
	fmt.Println("hello world")
}

func main() {
	sayHelloWorld()
}

这里就是调用了sayHelloWorld的函数,然后就会使用fmt包中的Println函数进行字符串的打印

如何运行这个脚本?两种方式:

  • go build hello.go 进行编译,然后运行编译后的可执行文件
  • go run hello.go 直接在内存中运行这个Go文件

在解决了上述的最基本的文件运行之后,进入到包管理的内容。

2.2 在不同位置调用函数

我们在同一个目录中创建一个main.go文件,我们尝试在main.go中调用hello.go中的sayHelloWorld函数

需要如下的写法:

hello.go中

package main

import "fmt"

func sayHelloWorld() {
	fmt.Println("hello world")
}

main.go中定义main函数

package main

func main() {
	sayHelloWorld()
}

然后运行就不能指定文件名称了,需要使用绝对路径或者相对路径的形式

image-20221226120017225

2.3 在不同包不同文件调用函数

我们在创建mod的包(文件夹)中,在创建一个子文件夹,我这里创建了一个testdir文件夹

test文件夹中,创建一个test.go的文件,然后在其中编写一个输出helloworld的函数。

我们的目的就是在main.go中调用testdir包中的test.go的helloworld函数

这样我们就得在main.go中import我们的testdir包

test.go

package testdir

import "fmt"

func SayHelloWorld() {
	fmt.Println("hello world")
}

main.go

package main

import "test/testdir"

func main() {
	testdir.SayHelloWorld()
}

目录结构

image-20221226120700167

这样执行go run main.go或者使用build进行编译就可以输出了

值得注意的是,在test.go中,一定要让SayHelloWorld的首字母S大写,因为Go中规定,本包中导出的函数首字母要大写。可以理解成Java中的public和private管理机制(或者Python中的下划线隐藏机制)

3.注释与转义字符

单行注释使用 //

多行注释使用 /* */

转义字符使用 \

还有常用的\n \r \t这里就不多赘述了,基本各个语言都是这种配置

4.变量与常量

一门语言最基本的东西之一辣。

4.1 变量

在Go中,有如下几种的变量形式

  • 使用var
  • 使用var并指定类型
  • 使用:=进行缩写
  • 批量申明

同时,需要注意,Go中定义的变量一定需要使用!

package main

import "fmt"

func main() {
	var name = "woodwhale"	// 自动推断为字符串
	var age int = 20		// 指定类型为int
	money := 1.14			// 不使用var而使用:=来进行缩写, 同时自动推断
	fmt.Printf("%s's age is %d and he has %f yuan", name, age, money)
}

上述的格式可以转为批量申明的格式

package main

import "fmt"

func main() {
	var (
		name  string  = "woodwhale"
		age   int     = 20
		money float64 = 1.14
	)	// 批量申明
	fmt.Printf("%s's age is %d and he has %f yuan", name, age, money)
}

上述演示的都是函数内的变量,如果需要使用全局的变量呢?在函数外定义就好了,不过函数外定义的变量不能使用:=的缩写形式

package main

import "fmt"

var name = "woodwhale"

func main() {
	var (
		age   int     = 20
		money float64 = 1.14
	)	// 批量申明
	fmt.Printf("%s's age is %d and he has %f yuan", name, age, money)
}

如果需要夸包调用变量呢?将首字母大写就可以了

4.2 常量

在Go中,使用关键字const来定义常量,常量和变量的区别就是,常量不能进行更改,定义的时候就固定了值了

package main

import (
	"fmt"
)

func main() {
	const (
		v1 = iota	// iota表示当前行数,从0开始
		v2 // 默认是上一行的值,也就是iota
		v3
		v4
		v5
		v6
	)
	fmt.Printf("v1 = %v\nv2 = %v\nv3 = %v\nv4 = %v\nv5 = %v\nv6 = %v\n", v1,v2,v3,v4,v5,v6)
}

5.数据类型

Go中所有的值的类型变量常量都会在声明时被分配内存空间并且被赋予默认值

前置基础知识:1字节 = 8位(1 byte = 8 bits)

5.1 整型

在计算机底层,数据都是二进制。

对于整数而言,也是如此,那么计算机如何判断负数和整数呢?可以通过第一位的标识位来判断(详细的原码、反码、补码等知识点看看计算机导论)

在Go中,和C语言一样,也具有无符号数与符号数

如下表所示,是Go中整数数据类型的属性:

名称长度范围默认值
int88 bits-128~1270
uint88 bits0~2550
int1616 bits-32768~327670
uint1616 bits0~655360
int3232 bits-2147483648~21474836470
uint3232 bits0~42949672960
int6464 bits-9223372036854775808~92233720368547758070
uint6464 bits0~184467440737095516160
int32 / 64 bits0
uint32 / 64 bits0

int和uint的比特位数取决于操作系统的位数,64位的机器那么int就是64位的

十进制:无需前缀

二进制:0b

八进制:0o

十六进制:0x

5.2 浮点型

名称长度符号+值数+尾数默认值
float3232 bits1+8+230
float6464 bits1+11+520

5.3 大数

使用big包,支持任意精度的整数、有理数、浮点数

big package - math/big - Go Packages

5.4 数值型数据的类型转换

目标类型(被转换的数据)

需要注意可能存在精度丢失问题

5.5 字符型

名称别名作用
byteuint8的别名ASCII
runeint32的别名UTF-8

单引号是字符型,双引号是字符串类型

5.6 布尔型

bool,在Go中占 1 bit,默认值是false

5.7 字符串

string,可以使用len()查看字符串的长度,默认值是空串,也就是""

注意,len()获取的字符串长度实际上是字符串所占的字节大小,如果是汉字或者特殊字符,需要判断编码形式来获取字节大小

UTF-8编码下,一个汉字占3个字节。如果我们需要统计字符串真实的长度,可以将其转为rune数组的形式然后获取len

package main

import (
	"fmt"
)

func main() {
	name := "木鲸"	// UTF-8 一个汉字三个字节
	fmt.Printf("%d\n", len(name))			// 6
	fmt.Printf("%d\n", len([]rune(name)))	// 2
	fmt.Printf("%c\n", []rune(name)[0])		// 木
}

5.8 指针类型

这个和C类似,加个*,就成了指针类型了

5.9 自定义数据类型

使用type xxx可以自定义数据类型

可以让其为一种已存在的数据类型,例如type myUint8 uint8,这样就可以让myUint8成为独立于uint8的一个自定义数据类型,虽然心知肚明的是两者的作用一样,但是类型转化需要强制类型转化

还有一种方式就是可以定义一个结构体,这一部分内容到结构体章节中详细讲解。类型为type xxx struct {}

5.10 类型别名

使用type myUint8 = uint8这种方式就可以指定myUint8uint8的类型别名,两种类型可以互相转换

6.指针

Go中保留了指针这一操作。指针这种东西用好了非常的舒爽,各种操作可以行云流水。但是一旦指针出现问题,那么也可能触发各种安全问题的产生。

在介绍指针之前,先来说说计算机中两种变量传递方式:值拷贝和值传递

  • 值拷贝:开辟一块新的内存空间,存放原值的副本,副本和原值互不干扰

  • 值传递:开辟一块新的内存空间,存放原值的内存地址,可以通过原值的内存地址来访问原值

6.1 Go中的指针

取地址符号: &(获取当前变量的地址)

取数值符号: *(获取指向的地址的值)

数据类型: *指向的类型

6.2 例子1

通过一个简单的例子来看看调用函数的值拷贝:

package main

import (
	"fmt"
)

func inc(n int) {
	fmt.Printf("n自增前的数值 --> %v\n", n)
	fmt.Printf("n自增前的地址 --> %v\n", &n)
	n++
	fmt.Printf("n自增后的数值 --> %v\n", n)
	fmt.Printf("n自增后的地址 --> %v\n", &n)
}

func main() {
	cnt := 0
	fmt.Printf("调用inc前的cnt的数值 --> %v\n", cnt)
	fmt.Printf("调用inc前的cnt的地址 --> %v\n", &cnt)
	inc(cnt)
	fmt.Printf("调用inc后的cnt的数值 --> %v\n", cnt)
	fmt.Printf("调用inc前的cnt的地址 --> %v\n", &cnt)
}

/*
    调用inc前的cnt的数值 --> 0
    调用inc前的cnt的地址 --> 0xc0000a6058
    n自增前的数值 --> 0
    n自增前的地址 --> 0xc0000a6080       
    n自增后的数值 --> 1
    n自增后的地址 --> 0xc0000a6080       
    调用inc后的cnt的数值 --> 0
    调用inc前的cnt的地址 --> 0xc0000a6058
*/

main中变量cnt的地址和inc函数形参变量n的地址完全不同,在调用inc函数的时候,将cnt进行值拷贝给了n,让n的值为0,同时具有自己的一块内存空间

6.2 例子2

如果我们想通过指针在inc函数中对cnt进行自增的操作呢?

只需要取cnt的地址传入,然后在inc函数中对这个地址上的值进行自增就行了

package main

import (
	"fmt"
)

func inc(n *int) {
	fmt.Printf("自增前的地址 --> %v\n", n)
	*n++	// 地址上的值++
	fmt.Printf("自增后的地址 --> %v\n", n)
}

func main() {
	cnt := 0
	prt := &cnt
	fmt.Printf("调用inc前的cnt的数值 --> %v\n", cnt)
	fmt.Printf("调用inc前的cnt的地址 --> %v\n", &cnt)
	inc(prt)
	fmt.Printf("调用inc后的cnt的数值 --> %v\n", cnt)
	fmt.Printf("调用inc前的cnt的地址 --> %v\n", &cnt)
}
/*
    调用inc前的cnt的数值 --> 0
    调用inc前的cnt的地址 --> 0xc0000a6058
    自增前的地址 --> 0xc0000a6058        
    自增后的地址 --> 0xc0000a6058        
    调用inc后的cnt的数值 --> 1
    调用inc前的cnt的地址 --> 0xc0000a6058
*/

6.3 例子3

在Go中,可以使用new()创建一个指针,这个指针上的数据默认是对应类型的默认值

package main

import (
	"fmt"
)

func main() {
	ptr := new(int)
	// *ptr = 114514 // 可以使用取值符号进行赋值操作
	fmt.Printf("prt这个指针上的值是 --> %v\n"+
		"ptr这个指针指向的地址是 --> %v\n"+
		"ptr这个指针所占用的地址是 --> %v", *ptr, ptr, &ptr)
}

/*
    prt这个指针上的值是 --> 0
    ptr这个指针指向的地址是 --> 0xc0000160b8  
    ptr这个指针所占用的地址是 --> 0xc00000a028
*/

7.fmt格式

插入一张讲解fmt格式化字符串的一些格式

在Go中可以使用``这个符号来进行完整字符串的输出,类似于Python中的''' '''

7.1 通用类型

%% --> 打印%

%v --> 打印值

%T --> 打印类型

7.2 整数类型

%d --> 十进制

%b --> 二进制

%o --> 八进制

%x --> 十六进制

%X --> 大写十六进制

%U --> U+四位16进制int32

%c --> Unicode对应的字符

%q --> 带单引号的Unicode对于的字符

7.3 浮点类型

%f --> 标准小数

%.2f --> 保留两位小数

%.f --> 保留0位小数

%5f --> 最小宽度为5的小数

%5.2f --> 最小宽度为5的保留两位的小数

%b --> 指数为2的幂的无小数科学计数法

%e --> 使用小写e的科学计数法

%E --> 使用大写E的科学计数法

%g --> 自动对宽度较大的数采用%e

%G --> 自动对宽度较大的数采用%E

%x --> 0x十六进制科学计数法

%X --> 0X十六进制科学计数法

7.4 布尔类型

%t --> true/false的单词

7.5 字符串(byte切片)

%s --> 按字符串输出

%q --> 带双引号对字符串进行输出

%x --> 每个byte按两位小写十六进制输出

%X --> 每个byte按两位大写十六进制输出

7.6 指针

%p --> 0x开头的十六进制地址

所有整数类型的格式化字符串都可以使用

8.条件判断

8.1 if...else...

Go中的if else没啥特别的,不过也有特别的

  • 不需要使用小括号包起来
  • if后面可以跟变量赋值等一个简短语句,一个分号后的才是判断的条件
  • {符号一定要写在if那一行的后面(对C用户不是很友好)

写一个简单的例子

package main

import (
	"fmt"
)

func main() {
	var pwd string
	fmt.Println("请输入密码")
    fmt.Scanln(&pwd)
	if pwd == "114514" {
		fmt.Println("hello henghengheng")
	} else if pwd == "admin" {
		fmt.Println("hello admin")
	} else {
		fmt.Println("sorry")
	}
}

如上的例子可以简写成如下形式,也就是在if后面输入简短的语句(如果是申明变量,那么这个变量的作用域仅仅在if else中),一个分号后的才是判断条件

package main

import (
	"fmt"
)

func main() {
	var pwd string
	fmt.Println("请输入密码")
	if fmt.Scanln(&pwd); pwd == "114514" {
		fmt.Println("hello henghengheng")
	} else if pwd == "admin" {
		fmt.Println("hello admin")
	} else {
		fmt.Println("sorry")
	}
}

8.2 switch...case...

Go中的switch默认省略了break,可以使用fallthrough来到下一个case判断

如下语句和上面的if else等效

package main

import (
	"fmt"
)

func main() {
	var pwd string
	fmt.Println("请输入密码")
	fmt.Scanln(&pwd)
	switch {
	case pwd == "114514":
		fmt.Println("hello henghengheng")
	case pwd == "admin":
		fmt.Println("hello admin")
	default:
		fmt.Println("sorry")
	}

}

8.3 for循环

Go中没有while循环,但是可以用for循环来做到while的事情

  • 无限循环(类似 while true)
  • 条件循环(类似 while + 条件)
  • 标准for循环(Go中的标准for循环不能加上小括号)
package main

import (
	"fmt"
)

func main() {
	// 无限循环 类似于 while true
	i := 0
	for {
		fmt.Print(i, "\t")
		i++
		if i == 10 {
			fmt.Println()
			break
		}
	}

	// 条件循环 类似 while 某个条件
	i = 0
	for i < 10 {
		fmt.Print(i, "\t")
		i++
	}
	fmt.Println()

	// 标准for循环
	for i := 0; i < 10; i++ { // 这里的i是for循环内的局部变量
		fmt.Print(i, "\t")
	}
	fmt.Println()
}

在Go的循环中,可以使用label标签来进行流程控制

在如下代码中,我们给最外层的for循环套了一个out标签,在i == 4 && j == 4的条件下最外层循环会break

package main

import (
	"fmt"
)

func main() {

out:
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			fmt.Print("* ")
			if i == 4 && j == 4 {
				break out
			}
		}
		fmt.Println()
	}
}

同时,Go支持goto的形式,虽然不推荐使用,但是他可以调用,goto到一个label的位置

如下就是使用goto的一个例子,再次提醒goto不推荐使用

package main

import (
	"fmt"
)

func main() {
	fmt.Print("1 ")
	fmt.Print("2 ")
	fmt.Print("3 ")
	if true {
		goto seven
	}
	fmt.Print("4 ")
	fmt.Print("5 ")
	fmt.Print("6 ")
seven:
	fmt.Print("7 ")
}

9.函数

9.1 函数参数

Go中,函数的形参有如下特点:

  • 函数可以传入0到多个参数,前n个传入的参数类型相同,只需要在第n个参数指明参数类型就好了
  • 不确定形参个数时可以使用...数据类型来生成形参切片

9.2 函数返回值

Go中,函数的返回值有如下特点:

  • 支持多个返回值
  • 可以给返回值变量命名

写一个函数例子,可以返回两个值,分别是两数之和和两数之差

package main

import (
	"fmt"
)

func main() {
	fmt.Println(addAndSub(1, 2))
}

func addAndSub(a, b int) (int, int) {
	sum := a + b
	sub := a - b
	return sum, sub
}

可以在函数的返回值中定义返回的变量名称,这样只需要写一个return就行

package main

import (
	"fmt"
)

func main() {
	fmt.Println(addAndSub(1, 2))
}

func addAndSub(a, b int) (sum, sub int) {
	sum = a + b
	sub = a - b
	return
}

在Go中,函数也是一种数据类型,实际上就是一个指针,函数名的本质是一个指向其函数内存地址的指针常量

函数 ≠ 调用函数,饭 ≠ 吃饭

9.3 匿名函数

拿来即用,没有命名的函数

package main

import (
	"fmt"
)

func main() {

	sum, sub := func(a, b int) (int, int) {
		sum := a + b
		sub := a - b
		return sum, sub
	}(1, 2)

	fmt.Printf("%v %v\n", sum, sub)
}

9.4 defer

defer是Go中的一个关键字,可以用来延迟执行某个函数

  • 延迟执行的函数会被压入栈中,等到return之后按照先进后出的顺序进行调用
  • 延迟执行的函数的参数仍然是会立即求值的

下面是一个延迟执行的例子:

首先是顺序执行的版本:

package main

import (
	"fmt"
)

func main() {
	f := deferUtil()

	f(1)
	f(2)
	f(3)
}

func deferUtil() func(int) int {
	i := 0
	innerFunc := func(n int) int {
		i++
		fmt.Printf("第%d次调用innerFunc\n", i)
		fmt.Printf("调用innerFunc的参数n --> %d\n", n)
		return i
	}

	return innerFunc
}

/*
    第1次调用innerFunc
    调用innerFunc的参数n --> 1
    第2次调用innerFunc        
    调用innerFunc的参数n --> 2
    第3次调用innerFunc        
    调用innerFunc的参数n --> 3
*/

如果使用defer的效果

package main

import (
	"fmt"
)

func main() {
	f := deferUtil()

	defer f(1)
	defer f(2)
	f(3)
}

func deferUtil() func(int) int {
	i := 0
	innerFunc := func(n int) int {
		i++
		fmt.Printf("第%d次调用innerFunc\n", i)
		fmt.Printf("调用innerFunc的参数n --> %d\n", n)
		return i
	}

	return innerFunc
}

/*
    第1次调用innerFunc
    调用innerFunc的参数n --> 3
    第2次调用innerFunc        
    调用innerFunc的参数n --> 2
    第3次调用innerFunc
    调用innerFunc的参数n --> 1
*/

可以看到,f(1)和f(2)都被延迟执行了,并且先执行f(2)再执行f(1),因为要遵守先进后出的原则。

那么这个defer到底有什么用呢?

还记得Java中烦人的关流的操作吗?在Go中可以使用defer迎刃而解!

package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.OpenFile("./flag.txt",os.O_WRONLY,0666)
	defer f.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	f.WriteString("flag{go_go_go}\n")
	fmt.Println("写入文件成功")
}

直接使用defer f.Close()就可以在最后关闭文件了

9.5 recover

Go中处理异常的try catch操作变成了defer recover的形式。

如何使用defer recover进行异常的抓捕呢?

package main

import (
	"fmt"
)

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("异常信息 --> %s\n", err)
		}
		fmt.Println("我抓捕了异常")
	}()
	fmt.Println("我正常执行")
	panic("我产生了异常")
	fmt.Println("我在异常之后不会执行")
}

Go中的defer recover并不等价于Java中的try catch,不能滥用

9.6 init函数

  • 每个包都可以用多个init函数
  • 执行顺序如下:
    • 被依赖包的全局变量
    • 被依赖包的init函数
    • ...
    • main包的全局变量
    • main包的init函数
    • main函数

因为其他包是不可能依赖main包的,所以main包的优先级最低,其余依赖包的执行顺序就看依赖关系了

10.数组

在各个语言中,数组都是不可缺少的一部分

10.1 申明数组

使用数组数据类型申明数组

package main

import (
	"fmt"
)

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

如果遇到了很长的数组,可以使用...来自动推断数组长度

package main

import (
	"fmt"
)

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

10.2 遍历数组

两种方式:

  • 标准for循环
  • for range

第一种标准for循环:

package main

import (
	"fmt"
)

func main() {
	a := [...]int{
		1,
		2,
		3,
		4,
		5,
		6,
		7,
	}
	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}
}

第二种,for range,可以使用_符号来占位

package main

import (
	"fmt"
)

func main() {
	a := [...]int{
		1,
		2,
		3,
		4,
		5,
		6,
		7,
	}
	for i, v := range(a) {
		fmt.Println(i, v)
	}
}

10.3 多维数组

多维数组申明的方式如下:(最外层可以使用...进行自动推断,内部的不行)

package main

import (
	"fmt"
)

func main() {
	a := [2][3]int {
		{1,2,3},
		{4,5,6},
	}
	for _, v := range(a) {
		for _, v2 := range(v) {
			fmt.Println(v2)
		}
	}
}

11.切片

有关切片的几个小知识点:

  • 切片是对数组的引用
  • 切片本身并不存储任何数据,他只是描述了底层数组中的一段
  • 切片的索引从0开始(切片没有负索引
  • 切片是引用类型,默认值是nil
  • 切片的遍历方式和数组相同

11.1 申明切片

申明切片有如下几种方式:

  1. 引用数组的一段
  2. 引用切片的一段
  3. 分配内存空间(make的方式)
package main

import (
	"fmt"
)

func main() {
	fmt.Println("切片申明的三种方式")

	// 1.通过数组申明切片, 从第一个元素到最后一个元素(默认省略)
	arr := [...]int {1,2,3,4,5,6,7,8,9,10}
	s1 := arr[1:]
	fmt.Printf("%v\n", s1)
    // 或者省略数组的生成, 让底层默认生成一个数组
    s1 = []int {1, 2, 3}
    fmt.Printf("%v\n", s1)

	// 2.通过切片申明切片
	s2 := s1[2:]
	fmt.Printf("%v\n", s2)

	// 3.通过make申明一段切片, 默认值都是0
	s3 := make([]int, 10)
	fmt.Printf("%v", s3)
}

/*
    切片申明的三种方式
    [2 3 4 5 6 7 8 9 10]
    [1 2 3]
    [3]
    [0 0 0 0 0 0 0 0 0 0]
*/

注意,数组基本类型是[x]int,这里的x是一个固定的数值,而切片的基本类型是引用类型,是[]int,没有确切的数值

11.2 修改切片的值

如果对切片某个索引的值进行修改,那么切片指向的这个地址上的真实数据也会被修改,同理,切片的切片对应的值也会修改。

package main

import (
	"fmt"
)

func main() {
	fmt.Println("修改切片的值")

	// 1.通过数组申明切片, 从第一个元素到最后一个元素(默认省略)
	arr := [...]int {1,2,3,4,5,6,7,8,9,10}
	s1 := arr[1:]
	fmt.Printf("%v\n", s1)

	// 2.通过切片申明切片
	s2 := s1[2:]
	fmt.Printf("%v\n", s2)

	// 如果对切片某个索引的值进行修改,那么切片指向的这个地址上的真实数据也会被修改
	// 例如, 修改上方s1[4] = 0, 那么arr[5] = 0
	s1[4] = 0
	fmt.Printf("修改切片s1[4] = 0后, arr = %v\n", arr)

	// 同理,切片的切片对应的指向的值也会修改
	fmt.Printf("修改切片s1[4] = 0后, s2 = %v\n", s2)
}

/*
    修改切片的值
    [2 3 4 5 6 7 8 9 10]
    [4 5 6 7 8 9 10]
    修改切片s1[4] = 0后, arr = [1 2 3 4 5 0 7 8 9 10]
    修改切片s1[4] = 0后, s2 = [4 5 0 7 8 9 10] 
*/

11.3 追加切片元素

使用append()函数进行切片元素的添加

package main

import (
	"fmt"
)

func main() {
	// 申明一个切片(底层自动创建了一个数组)
	arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

	// 追加切片元素, 并赋予返回值给原变量arr
	arr = append(arr, 100, 101, 102)

	// 打印arr
	fmt.Printf("%v\n", arr)

	// 追加切片元素的第二种方式, 直接追加一个切片, 使用切片+...的方式
	arr = append(arr, arr...)

	// 打印arr
	fmt.Printf("%v\n", arr)
}

11.4 复制切片

使用copy()函数进行切片的复制

package main

import (
	"fmt"
)

func main() {
	// 申明切片(底层自动创建了一个数组)
	arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	arr2 := []int{9, 8, 7}

	// 把arr2中的内容复制到arr中
	copy(arr, arr2)

	fmt.Printf("%v\n", arr)
}

/*
    [9 8 7 4 5 6 7 8 9 10]
*/

12.map

有关map需要知道的几个点:

  • map的本质是无序的键值对
  • map是引用类型,默认值是nil
  • map的容量可以提前申明,超过容量会自动触发扩容

12.1 map的申明与使用

map的声明数据类型为map[key_type]value_type

例如map[string]int

map的生成使用make()函数来完成或者直接声明

map的使用是mp[key]=value的形式,其中这个mp是一个map对象

下面是一个简单的例子

package main

import (
	"fmt"
)

func main() {
    mp := makeOneMap()

    fmt.Println(mp)
}

func makeOneMap() (mp map[string]int) {
    mp = make(map[string]int, 1)
    mp["wood"] = 20
    // 上述操作等同于
    // mp = map[string]int {"wood" : 20}
    return
}

12.2 map的查找与删除

获取map的值的时候可以获取两个返回值,第二个值是bool类型,如果存在该键对应的值,那么为true,否则是false

可以使用delete()函数指定map中的某个键被删除

package main

import (
	"fmt"
)

func main() {
    mp := makeOneMap()
    // 判断map中是否存在一个键值对
    if value, flag := mp["wood"]; flag {
        fmt.Printf("该map存在键为wood的值是 --> %v\n", value)
    }
    // 删除map中的一个元素
    delete(mp, "wood")

    if value, flag := mp["wood"]; flag {
        fmt.Printf("该map存在键为wood的值是 --> %v\n", value)
    } else {
        fmt.Println("不存在key")
    }
}

func makeOneMap() (mp map[string]int) {
    mp = make(map[string]int, 1)
    mp["wood"] = 20
    // 上述操作等同于
    // mp = map[string]int {"wood" :20}
    return
}

/*
    该map存在键为wood的值是 --> 20
    不存在key
*/

12.3 map的for...range...遍历

map只能使用for...range...的方式进行遍历

package main

import (
	"fmt"
)

func main() {
    mp := make(map[string]int, 1)
    mp["wood"] = 20
    mp["sheep"] = 22
    for key, value := range mp {
        fmt.Printf("%v --> %v\n", key, value)
    }
}

13.结构体(面向对象)

说实话,Go中保留了结构体我没想到,同时Go中没有class之类的定义,所以Go中的面向对象是通过结构体来实现的。

13.1 结构体声明

在Go中定义一个结构体很简单,使用如下的语句就可以定义

// student结构体
type Student struct {
	name string
	age  int
}

其中使用属性名+类型就可以定义一个属性

生成结构体变量也很简单

package main

import (
	"fmt"
)

// student结构体
type Student struct {
	name string
	age  int
}

func main() {
	s1 := Student{ // 实例化一个结构体
		name: "woodwhale",
		age:  20,
	}
	fmt.Println(s1)

	s2 := &Student{ // 实例化一个结构体, 并将s2指向这个结构体实例
		name: "sheepbotany",
		age:  22,
	}
	fmt.Println(s2)
}

/*
    {woodwhale 20}
    &{sheepbotany 22}
*/

13.2 结构体字段

对于一个结构体变量,可以使用.的方式来访问这个结构体的属性

package main

import (
	"fmt"
)

// student结构体
type Student struct {
	name string
	age  int
}

func main() {
	s1 := Student{ // 实例化一个结构体
		name: "woodwhale",
		age:  20,
	}
	fmt.Println(s1)

	s2 := &Student{ // 实例化一个结构体, 并将s2指向这个结构体实例
		name: "sheepbotany",
		age:  22,
	}
	fmt.Println(s2)

    fmt.Printf("%v's age is %v\n", s1.name, s1.age)

}

/*
    {woodwhale 20}
    &{sheepbotany 22}
    woodwhale's age is 20
*/

13.3 结构体指针

结构体对应的也有指针类型

结构体指针访问属性可以简写,不需要使用(*value).name这种先取值获取结构体真实变量再去访问属性,可以使用value.name这种方式快速访问

package main

import (
	"fmt"
)

// student结构体
type Student struct {
	name string
	age  int
}

func main() {
	s1 := Student{ // 实例化一个结构体
		name: "woodwhale",
		age:  20,
	}
	fmt.Println(s1)

	s2 := &Student{ // 实例化一个结构体, 并将s2指向这个结构体实例
		name: "sheepbotany",
		age:  22,
	}
	fmt.Println(s2)

    fmt.Printf("%v's age is %v\n", s1.name, s1.age)
    fmt.Printf("%v's age is %v\n", s2.name, s2.age) // 省略形式调用, 等效于(*s2).name
}

/*
    {woodwhale 20}
    &{sheepbotany 22}
    woodwhale's age is 20
    sheepbotany's age is 22
*/

13.4 结构体继承

继承这一个概念属于面向对象中的,在Go中,由于结构体代替了class,所以继承也放在了结构体中。

想要继承一个结构体,将该结构体的一个属性设置为想继承的结构体

package main

import (
	"fmt"
)

// student结构体
type Student struct {
	name string
	age  int
}

// 继承student
type SuperStudent struct {
    Student
    money int
}

func main() {
	s1 := Student{ // 实例化一个结构体
		name: "woodwhale",
		age:  20,
	}
	fmt.Println(s1)

	s2 := &Student{ // 实例化一个结构体, 并将s2指向这个结构体实例
		name: "sheepbotany",
		age:  22,
	}
	fmt.Println(s2)

    s3 := SuperStudent {
        Student: s1,
        money: 100,
    }
    fmt.Println(s3)
}

/*
    {woodwhale 20}
    &{sheepbotany 22}
    {{woodwhale 20} 100}
*/

上述代码中SuperStudent继承了Student中的属性,是可以访问name、age的,还有独立的属性money

还可以使用*Student的方式来继承

13.4 结构体方法

在面向对象编程中,一个类的对象具有其特有的方法。

在Go中,想要实现某个对象的特有方法,可以使用结构体方法来申明。

申明结构体方法也很简单,在方法名前面加上特定的结构体对象

func (接收参数名 类型)方法名(形参列表)返回值列表{}

package main

import (
	"fmt"
)

// student结构体
type Student struct {
	name string
	age  int
}

func (s *Student) entrance(activity string) {
	fmt.Printf("%v岁的%v%v了", s.age, s.name, activity)
}

func main() {
	s1 := Student{ // 实例化一个结构体
		name: "woodwhale",
		age:  20,
	}
	s1.entrance("上学") // 调用结构体方法
}

/*
    20岁的woodwhale去上学了
*/

因为结构体是可以继承的,所以继承后的结构体可以继承相对应的结构体方法,如下面的案例所示,SuperStudent继承了Studententrance方法

package main

import (
	"fmt"
)

// student结构体
type Student struct {
	name string
	age  int
}

func (s *Student) entrance(activity string) {
	fmt.Printf("%v岁的%v%v了", s.age, s.name, activity)
}

type SuperStudent struct {
	*Student
	privilege string
}

func main() {
	s1 := SuperStudent{ // 实例化一个结构体
		Student: &Student{
			name: "woodwhale",
			age:  20,
		},
		privilege: "睡觉",
	}
	s1.entrance("上学") // 调用结构体方法
}

需要注意的是,结构体方法和结构体需要写在同一个包

13.5 接口

在Go中,接口是一种特殊的数据类型

可以使用如下形式来定义接口

type  接口名称 interface {
    method1(参数列表) 返回值列表
    method2(参数列表) 返回值列表
    ...
    methodn(参数列表) 返回值列表
}

go中无需"implements"关键字,一个类型实现了接口的所有方法即实现了该接口(duck typing)

下面是一个使用接口的例子

package main

import (
	"fmt"
)

type Message interface {
	setContent()
}

type TextMessage struct {
	textConetnt string
}

type ImageMessage struct {
	imgContent string
}

func (msg *TextMessage) setContent() {
	msg.textConetnt = "test text"
}

func (msg *ImageMessage) setContent() {
	msg.imgContent = "setu"
}

func (msg *TextMessage) sendText() {
	fmt.Printf("发送TextMessage --> %v\n", msg.textConetnt)
}

func (msg *ImageMessage) sendImg() {
	fmt.Printf("发送ImageMessage --> %v\n", msg.imgContent)
}

func send(msg Message) {
	fmt.Println("准备发送消息")
	msg.setContent()
	switch mptr := msg.(type) {
	case *TextMessage:
		mptr.sendText()

	case *ImageMessage:
		mptr.sendImg()
	}
}

func main() {
	text := TextMessage{}
	img := ImageMessage{}

	send(&text)
	send(&img)
}

使用switch…case + interface.(type)进行类型选择

还有一种类型断言的方式:

func send(msg Message) {
	fmt.Println("准备发送消息")
	msg.setContent()
	if text, ok := msg.(*TextMessage); ok {
		text.sendText()
	}
	if img, ok := msg.(*ImageMessage); ok {
		img.sendImg()
	}
}

最后,interface{}可以声明一个空接口,空接口可以保存任何的类型,Go中any是空接口的类型别名

14. 协程

Go以简单的协程出名,使用协程非常的简单,只需要一个go关键字就能以一个协程运行函数

14.1 启动协程

非常的简单,看如下的例子

package main

import (
	"fmt"
)

func main() {
	go func(n int) {
		fmt.Printf("%v\t", n)
	}(100)	// 启动一个协程

	fmt.Scanln() // 等待协程启动完,否则主线程结束了协程还没执行完毕
}

14.2 channel管道

channel一般配合goroutine使用,channel在存完数据后需要使用close进行关闭。

申明方式如下:

chan1 := make(chan int, 100) // 100可省略,是管道初始大小

将数据存入管道的方式

chan1 <- value

将数据从管道中取出的方式

value, ok := <- chan1	// value是值,ok是取出的状态,true就取出成功

下面是三个例子表示三种不同的取值方式

package main

import (
	"fmt"
)


func calcPrime(n int, c chan int) {
	for i := 2; i <= (n / 2); i++ {
		if n%i == 0 {
			return
		}
	}
	c <- n // 把n推进c这个管道
}

func main() {
	can := make(chan int)
	for i := 2; i <= 100001; i++ {
		go calcPrime(i, can)
	}
	// 第一种从管道取值的方式,由于管道没有关闭所以会报错
	for {
		val, ok := <- can
		if ok {
			fmt.Printf("%v\t", val)
		}
	}

	// 第二种使用for...range...的方式遍历管道,由于管道没有关闭所以会报错
	for val := range can {
		fmt.Printf("%v\t", val)
	}

	// 第三种,使用select...case...来判断管道是否还有东西
outer:
	for {
		select {
		case val, ok := <-can:
			if ok {
				fmt.Printf("%v\t", val)
			}
		default:
			fmt.Println("管道已经取完")
			break outer
		}
	}

}

如上代码只有第三种select...case...的模式才能成功运行不报错,因为没有在协程函数中对channel进行close。

select…case 会阻塞到某个分支可以继续执行时执行该分支,当没有可执行的分支时则执行 default 分支

后话

本文讲述了Go语言的基本使用方式,更进阶的方式笔者也还有没有学到位,包括Go中常用的包、常用的编程手法都没有进行介绍。有机会笔者将会把这些未学的内容补齐。