【Go】入门Go语言

news2024/11/15 6:46:29

【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中常用的包、常用的编程手法都没有进行介绍。有机会笔者将会把这些未学的内容补齐。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/141622.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

idea配置maven步骤及常见问题

idea 配置maven步骤及常见问题maven 下载maven的配置配置系统环境变量maven本地仓库配置和镜像加速idea 中配置maven的设置常见问题&#xff0c;每次新建项目需要重新手动配置maven的解决maven 下载 首先&#xff0c;进入它的官网&#xff1a;链接: https://maven.apache.org/…

Find My资讯|被盗的AirPods通过苹果Find My找回并抓到盗窃者

美国一位盗窃案的受害者&#xff0c;他在圣安东尼奥莱昂谷的家中发现有人偷走了他的汽车后决定亲手将小偷捉住。打开Find My&#xff0c;他看到其中被拿走的AirPods显然是停在35号州际公路上的一个旅行站。在前往该站并发现一辆SUV里有五个人后&#xff0c;阿林顿打电话给警察寻…

谷粒学院——第二十二章、Jenkins可持续自动部署

一、安装内容 Jenkins&#xff08;本文主要安装&#xff09;、Maven、Git、JDK Jenkins与Github配合实现持续集成需要注意以下几点&#xff1a; ①Jenkins要部署到外网&#xff0c;因为内网Github是无法访问到的&#xff08;走过的坑&#xff01;&#xff09;&#xff0c;这里…

Usaco Training 刷怪旅 第三层 第六题:Ski Course Design

说实话&#xff0c;一开始看上一题的时候觉得太恶心就先来做这道&#xff0c;所以这题其实比上一题早做出来&#xff08;&#xff09; Farmer John has N hills on his farm (1 < N < 1,000), each with an integer elevation in the range 0 .. 100. In the winter, sin…

MVC MVVM架构

注&#xff1a;个人理解仅供参考1、MVC优点&#xff1a;1、模块独立&#xff0c;解耦缺点&#xff1a;1、厚重的ViewController2、遗失&#xff08;无处安放&#xff09;的网络逻辑3、较差的可测试性图2、MVVM优点1、低耦合&#xff1a;View可以独立于Model变化和修改&#xff…

【HybirdCLR】入门记录-Unity2021 + WebGL

目录前言环境案例学习先PC平台试一下转为WebGL平台动手做一个demo功能基本工作流程搭建环境构建项目补充致谢参考资料前言 之前一直有听说热更新技术&#xff0c;于是找点时间来研究一下热更新技术的使用。热更新的实现方式有很多种&#xff0c;这里笔者记录一下自己学习Hybir…

基于开源体系的云原生微服务治理实践与探索

作者&#xff1a;董艺荃&#xff5c;携程服务框架负责人 携程微服务产品的发展历程 携程微服务产品起步于 2013 年。最初&#xff0c;公司基于开源项目 ServiceStack 进行二次开发&#xff0c;推出 .Net 平台下的微服务框架 CServiceStack。 2014 年&#xff0c;公司推出 Jav…

【PhD Debate —11】矛与盾的对决——神经网络后门攻防

点击蓝字关注我们AI TIME欢迎每一位AI爱好者的加入&#xff01;2022年7月9日&#xff0c;AI TIME组织了Ph.D. Debate第十一期&#xff0c;题为“矛与盾的对决——神经网络后门攻防”的研讨活动&#xff0c;特别邀请了宾夕法尼亚州州立大学电子工程系博士生向臻、清华大学博士生…

区块链基础知识(一)

参考书籍《区块链原理、设计与应用》 基本原理 比特币网络工作流程 技术的演化与分类 分布式共识 交易性能 扩展性问题 数据库和存储系统 数据库也是区块链中重要的一环 分布式系统核心问题 单节点结构演变为分布式系统&#xff0c;首先遇到的问题就是一致性的保障。 一致性问…

小程序开发经验分享(5)-全屏展示小程序

小程序的 navigationBar,有APP开发经验的同学应该知道,navigationBar也就是程序最顶部的一条,我们同常的开发页面,也都是在navigationBar下方的主体区用标签开发UI部分 如下面两个图片,左边的翻译君官方小程序中上面就有这样一个navigationBar用于展示自己的title和菜单按…

K8s 认证工程师 CKA 考题分析和题库练习(下)

目录 10、考核知识&#xff1a;检查可用节点数量 题目内容 题目内容中文解释 做题解答 11、考核知识&#xff1a;一个 Pod 封装多个容器 题目内容 题目内容中文解释 官方文档搜索关键字&#xff1a;pod ​编辑做题解答 12、考核知识&#xff1a;持久卷 PersistentV…

Mac M1芯片 搭建RocketMQ

Mac M1芯片 搭建RocketMQ RocketMQ需要启动三个组件 1、nameserver 路由中心,为整个MQ集群提供服务协调与治理 2、broker 提供消息的转发和存储功能 3、console 控制台面板工具 1、下载RocketMQ https://rocketmq.apache.org/dowloading/releases/ 选择 Binary: rocketmq-all-4…

对外投资追踪汇率数据金融开放指数

一、2005-2020年中国全球投资追踪数据 “中国全球投资跟踪”&#xff08;China Global Investment Tracker&#xff09;数据库&#xff0c;由美国企业研究所于1月28日发布。该数据库是唯一一套涵盖中国全球投资和建设的综合数据集&#xff0c;同时使用分别记录和汇总记录两种方…

这几个算法可视化网站,太牛了!

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/Tyson0314/Java-…

深入 Android 底层服务(service)

前言 我们都知道&#xff0c;字节最近发布了PICO&#xff14;VR眼镜&#xff0c;我买了一个&#xff0c;体验还行。因为我也是做VR眼镜的Android应用层开发的&#xff0c;所以想把自己项目中遇到的一些Android技术分享给读者。近些年随着VR眼镜的兴起&#xff0c;Android的的服…

项目经理跨部门沟通,如何避免踢皮球?

软件项目干系人越多&#xff0c;沟通成本也就越高&#xff0c;非常容易出现相互提皮球的现象。那么如何高效沟通&#xff1f; 1、划分干系人 不同干系人不同策略 软件项目涉及干系人众多&#xff0c;项目不经理不可能对每一个人进行详细沟通&#xff0c;那么我们需要将干系人的…

如何把优化Docker镜像大小

什么是Docker Docker是一个开源的虚拟化平台&#xff0c;可以让开发人员将应用程序和依赖项打包在轻量级容器中&#xff0c;然后可以轻松地在任何环境中运行。这样&#xff0c;开发人员可以将容器作为独立的可移植单元在不同的环境中部署和运行应用程序&#xff0c;而不用担心环…

系统学习ElasticSearch

1.1 、ElasticSearch&#xff08;简称ES&#xff09; Elasticsearch是用Java开发并且是当前最流行的开源的企业级搜索引擎。 能够达到实时搜索&#xff0c;稳定&#xff0c;可靠&#xff0c;快速&#xff0c;安装使用方便。 客户端支持Java、.NET&#xff08;C#&#xff09;、…

SAP FICO财务月结-外币评估

月末操作-外币评估 —文章整理自高林旭老师的《由浅入深学习SAP财务》一书&#xff0c;SAP相关从业人员值得一读。 企业的外币业务在记账的时候一般都是使用期初的汇率或者即时汇率&#xff0c;但是在月末&#xff0c;需要按照月末汇率对外币的余额或者未清项进行重估&#xf…

c语言入门-3-打印复杂类型

打印复杂类型前言上代码字符整形浮点型打印超长小数向内存中申请空间局部变量&#xff0c;全局变量使用输入函数 scanf作用域生命周期深度解析1 c语言中数据类型2 这些字段类型的大小又是多少呢3 计算机的大小单位4 scanf 报错下一篇前言 语言本身的学习&#xff0c;有两点比较…