Go入门Go语言

Posted woodwhale

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go入门Go语言相关的知识,希望对你有一定的参考价值。

【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()

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

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()

目录结构

这样执行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.<

以上是关于Go入门Go语言的主要内容,如果未能解决你的问题,请参考以下文章

go语言入门详细教程:时代下的 go 语言

Go介绍

Go语言入门

Go语言从入门到规范-3.1Go并发

Go语言入门

Go语言入门