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中整数数据类型的属性:
名称 | 长度 | 范围 | 默认值 |
---|---|---|---|
int8 | 8 bits | -128~127 | 0 |
uint8 | 8 bits | 0~255 | 0 |
int16 | 16 bits | -32768~32767 | 0 |
uint16 | 16 bits | 0~65536 | 0 |
int32 | 32 bits | -2147483648~2147483647 | 0 |
uint32 | 32 bits | 0~4294967296 | 0 |
int64 | 64 bits | -9223372036854775808~9223372036854775807 | 0 |
uint64 | 64 bits | 0~18446744073709551616 | 0 |
int | 32 / 64 bits | 0 | |
uint | 32 / 64 bits | 0 |
int和uint的比特位数取决于操作系统的位数
,64位的机器那么int就是64位的
十进制:无需前缀
二进制:0b
八进制:0o
十六进制:0x
5.2 浮点型
名称 | 长度 | 符号+值数+尾数 | 默认值 |
---|---|---|---|
float32 | 32 bits | 1+8+23 | 0 |
float64 | 64 bits | 1+11+52 | 0 |
5.3 大数
使用big包,支持任意精度的整数、有理数、浮点数
big package - math/big - Go Packages
5.4 数值型数据的类型转换
目标类型(被转换的数据)
需要注意可能存在精度丢失问题
5.5 字符型
名称 | 别名 | 作用 |
---|---|---|
byte | uint8的别名 | ASCII |
rune | int32的别名 | 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
这种方式就可以指定myUint8
是uint8
的类型别名,两种类型可以互相转换
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语言的主要内容,如果未能解决你的问题,请参考以下文章