Golang开发手册
官方文档
环境配置
Windows下多平台交叉编译
Go Install工具应用
go install
工具为公共代码仓库中维护的开源代码而设计。
无论是否公布代码,该模型设置工作环境的方法都是相同的。
Go代码必须放在工作空间内,其实就是一个目录,其中包含三个子目录:
✅src 目录包含Go的源文件,它们被组织成包(每个目录都对应一个包)
✅pkg 目录包含包对象
✅bin 目录包含可执行程序
go 工具用于构建源码包,生成二进制文件到 bin 目录中。
GOPATH 环境变量
GOPATH 环境变量指定了工作空间位置。
工作空间可以放在任何地方, 但是它绝对不能和Go安装目录相同。
首先创建一个工作空间目录,并设置相应的 GOPATH
。
$ mkdir $HOME/work
$ export GOPATH=$HOME/work
作为约定,请将此工作空间的 bin
子目录添加到你的 PATH
中
export PATH=$PATH:$GOPATH/bin
包路径
标准库中的包有给定的短路径,比如 "fmt"
和 "net/http"
。
自己的包必须选择一个基本路径,保证不和标准库和其它第三方库冲突。
用 github.com/user
作为基本路径。在工作空间里创建一个目录, 将源码存放到其中:
$ mkdir -p $GOPATH/src/github.com/wintrysec
Go mod
Go Module与 maven 类似是一个包管理工具,控制依赖包版本。
go mod init 项目名称 #初始化当前文件夹,创建go.mod文件
go mod tidy #同步包状态,增加缺少的包删除无用的包
go mod verify #校验依赖
流程控制
流程控制在编程语言中是最伟大的发明,有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。
Go中流程控制分三大类:条件判断,循环控制、无条件跳转
条件判断 if
Go的if
还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了.
if integer := computedValue(); integer == 3 {
fmt.Println("The integer is equal to 3")
} else if integer < 3 {
fmt.Println("The integer is less than 3")
} else {
fmt.Println("The integer is greater than 3")
}
无条件跳转 goto
用goto
跳转到必须在当前函数内定义的标签.
func myFunc() {
i := 0
Here: //这行的第一个词,以冒号结束作为标签(大小写敏感)
println(i)
i++
goto Here //跳转到Here去
}
循环控制 for
它即可以用来循环读取数据,又可以当作while
来控制逻辑,还能迭代操作。
sum := 0;
for index:=0; index < 10 ; index++ {
sum += index
}
fmt.Println("sum is equal to ", sum)
//index:=0 在循环开始之前调用
//index++ 在每轮循环结束之时调用
while
的功能
其中;
可以省略,就变成了While
sum := 1
for sum < 1000 {
sum += sum
}
注意点
1)在循环里面有两个关键操作,break
操作是跳出当前循环,continue
是跳过本次循环。
2)可以使用_
来丢弃不需要的返回值
for _, v := range map{
fmt.Println("map's val:", v)
}
Switch
有些时候你需要写很多的if-else
来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候switch
就能很好的解决这个问题。
i := 10
switch i {
case 1:
fmt.Println("i is equal to 1")
case 2, 3, 4:
fmt.Println("i is equal to 2, 3 or 4")
case 10:
fmt.Println("i is equal to 10")
default:
fmt.Println("All I know is that i is an integer")
}
Go里面switch
默认相当于每个case
最后带有break
,匹配成功后不会自动向下执行其他case,而是跳出整个switch
, 但是可以使用fallthrough
强制执行后面的case代码。
Struct
结构体可以更方便的访问数据,且支持匿名字段。
匿名字段能够实现字段的继承,所有的内置类型和自定义类型都是可以作为匿名字段。
type Human struct {
name string
age int
phone string // Human类型拥有的字段
}
type Employee struct {
Human // 匿名字段Human,不带数据类型
speciality string
phone string // 雇员的phone字段
}
func main() {
Bob := Employee{Human{name:"Bob", age:34, phone:"777-444-XXXX"}, speciality:"Designer", phone:"333-222"}
//访问结构体数据
fmt.Println("Bob's work phone is:", Bob.phone)
// 如果我们要访问Human的phone字段
fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}
interface
Go语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便.
简单的说,interface是一组method签名的组合,我们通过interface来定义对象的一组行为。
空interface
空interface(interface{})不包含任何的method,它可以存储任意类型的数值。
// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s
Comma-ok断言
断言可以直接判断是否是该类型的变量。
for index, element := range list {
if value, ok := element.(int); ok {
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
} else if value, ok := element.(string); ok {
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
}else {
fmt.Printf("list[%d] is of a different type\n", index)
}
}
嵌入
如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。
type Interface interface {
sort.Interface //嵌入字段sort.Interface
Push(x interface{}) //a Push method to push elements into the heap
Pop() interface{} //a Pop elements that pops elements from the heap
}
看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less returns whether the element with index i should sort
// before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
反射
所谓反射就是能检查程序在运行时的状态。我们一般用到的包是reflect包。
1)使用reflect一般分成三步,要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。
t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值
转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如
tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值
获取反射值能返回相应的类型和数值
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
如果要修改相应的值,必须这样写
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
字符串操作
fmt包
fmt包实现了类似C语言printf和scanf的格式化I/O。Print系列函数会将内容输出到系统的标准输出。
//直接输出内容
fmt.Print("在终端打印该信息")
//格式化输出字符串
fmt.Printf("Hello %s\n", "World")
//在输出内容的结尾添加一个换行符
fmt.Println("在终端打印单独一行显示")
//格式化字符串不输出,相当于变量
fmt.Sprintf("%s:%v", ip, port)
strings包
修剪字符串
x := "!!@@@你好!!,@@@ Gophers@@@!!"
//将字符串左侧和右侧中匹配 cutset 中的任一字符的字符去掉
fmt.Println(strings.Trim(x, "@!"))
//将字符串左侧中匹配 cutset 中的任一字符的字符去掉
fmt.Println(strings.TrimLeft(x, "@!"))
//将字符串右侧中匹配 cutset 中的任一字符的字符去掉
fmt.Println(strings.TrimRight(x, "@!"))
//输出
你好!!,@@@ Gophers
你好!!,@@@ Gophers@@@!!
!!@@@你好!!,@@@ Gophers
Contains
判断是否存在某个子字符串
str := "test1234helloak47888ak47886"
if strings.Contains(str, "hello") {
fmt.Println("子字符串在 str变量中 返回true")
} else {
fmt.Println("子字符串 不在 str变量中 返回false")
}
Count计算某个子字符串出现的次数
fmt.Println(strings.Count(str, "ak47"))
字符串分割
Split把字符串分割为字符串数组
fmt.Printf("%q\n", strings.Split("foo,bar,baz", ","))
fmt.Printf("%q\n", strings.SplitAfter("foo,bar,baz", ","))
//输出
["foo" "bar" "baz"]
["foo," "bar," "baz"]
//也就是说,Split 会将 s 中的 sep 去掉,而 SplitAfter 会保留 sep分隔符
fmt.Printf("%q\n", strings.SplitN("foo,bar,baz", ",", 2))
//带 N 的方法可以通过最后一个参数 n 控制返回的结果中的 slice 中的元素个数,最后一个元素不会分割
["foo" "bar,baz"]
字符串是否有某个前缀或后缀
//字符串是否以Go开头
fmt.Println(strings.HasPrefix("Gopher", "Go"))
//字符串是否以go结尾
fmt.Println(strings.HasSuffix("Amigo", "go"))
字符串 JOIN 操作
将字符串数组(或 slice)连接起来可以通过 Join 实现
fmt.Println(Join([]string{"name=xxx", "age=xx"}, "&"))
//输出 name=xxx&age=xx
将字符串重复几次
fmt.Println("ba" + strings.Repeat("na", 2))
//输出 banana
字符串字串替换
fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2))
fmt.Println(strings.ReplaceAll("oink oink oink", "oink", "moo"))
//输出
oinky oinky oink
moo moo moo
大小写转换
strings.ToLower()
strings.ToUpper()
strconv包
strconv包实现了基本数据类型与其字符串表示的转换,主要有: Atoi()、Itia()、parse系列等
str1 := "100"
//将字符串类型的整数转换为int类型
myNum, err := strconv.Atoi(str1)
//将int类型数据转换为对应的字符串表示
num1 := 200
str2 := strconv.Itoa(num1)
Parse系列函数
Parse类函数用于转换字符串为给定类型的值:ParseBool()、ParseFloat()、ParseInt()、ParseUint()
文件读取性能
Go语言提供了多种读写文件的方式
1)使用os包中的Open()和Close()函数,配合Read()和Write()方法读写文件。
此方式较为底层,需手动管理文件的打开和关闭及数据的缓冲区,但具有很高的灵活性和效率。
2)使用bufio包中的NewReader()和NewWriter()方法读写文件。
此方式比较高层,会自动进行数据的缓存,可以大幅提升读写效率。
3)使用ioutil包中的ReadFile()和WriteFile()方法读写文件。
此方式非常简单,适合用于一次性读取或写入整个文件内容,但是可能会出现内存占用过高的问题。
下面是几种方式的性能对比(测试文件大小为100MB)
读写方式 | 读取时间 | 写入时间 |
---|---|---|
os.Read() 和 os.Write() | 0.25s | 0.25s |
bufio.NewReader()和 bufio.NewWriter() | 0.1s | 0.1s |
ioutil.ReadFile()和 ioutil.WriteFile() 已经弃用 | 0.6s | 0.6s |
可以看出,使用bufio包中的方法读写文件的性能最佳,而使用ioutil包中的方法读写文件的性能最差。但是需要注意的是,性能测试结果可能因系统环境、文件大小、文件类型等因素而有所不同,具体使用时还需要进行实际测试。
文件写入操作方式上的差异
1)打开文件,写入内容,关闭文件。如此重复多次
2)打开文件,写入内容,defer 关闭文件。如此重复多次
3)打开文件,重复多次写入内容,defer 关闭文件
第一种慢但是稳定,第二种压栈太多导致崩溃报错,第三种速度快减少了很多打开关闭文件的操作
bufio读写
读取
var Usernames []string
//打开文件
file, err := os.OpenFile(filepath, os.O_RDONLY, 0666)
if err != nil {
return nil
}
defer file.Close()
//创建读取器
reader := bufio.NewReader(file)
for {
line, _, err := reader.ReadLine()
if err == io.EOF {
break
}
Usernames = append(Usernames, string(line))
}
写入
// 打开文件以进行写入
Logpath := "文件路径"
file, err := os.OpenFile(Logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
fmt.Printf("Open File failed for %v. Error: %s", Logpath, err.Error())
}
defer file.Close()
// 创建一个新的 writer 对象
writer := bufio.NewWriter(file)
// 写入字符串
_, err = writer.WriteString("text Content")
if err != nil {
fmt.Printf("Writer failed %v. Error: %s", Logpath, err.Error())
}
// 将缓冲区中的数据刷新到磁盘中
err = writer.Flush()
if err != nil {
fmt.Println(err)
}
HTTP客户端
GET请求
func main() {
url := "https://www.baidu.com/"
//请求参数设置
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Connection", "close")
req.Header.Set("Cookie", "rememberMe=1")
//设置http客户端参数
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //忽略https验证
}
client := &http.Client{
Transport: tr,
Timeout: time.Duration(8) * time.Second, //设置超时连接
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse /* 不进入重定向 */
},
}
//发送请求
resp, err := client.Do(req)
if err != nil {
// handle errorcls
}
//关闭网络连接
defer resp.Body.Close()
//读取HTTP响应体
body, _ := io.ReadAll(resp.Body)
fmt.Printf(string(body))
}
POST请求
基本的客户端设置都和GET方法一样
发送JSON数据
//要发送的JSON数据
data := map[string]interface{}{
"query": domain, //替换为域名
"fields": []string{"parsed.names", "parsed.extensions.subject_alt_name.dns_names"},
"flatten": true,
}
//JSON序列化
jsonData, err := json.Marshal(data)
if err != nil {
panic(err)
}
// 请求参数设置
req, _ := http.NewRequest("POST", URL, bytes.NewBuffer(jsonData))
req.SetBasicAuth(apiID, apiSecret) //Basic认证
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
发送form表单数据
//数据
payload := url.Values{}
payload.Set("foo", "a")
payload.Add("foo", "b")
payload.Set("foo2", "c")
// 请求参数设置
req, _ := http.NewRequest("POST", URL, strings.NewReader(payload.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
JSON解析
json.NewDecoder 和 json.Unmarshal 都可以将 JSON 数据解码为 Go 中的结构体。
解析到interface
如果我们不知道被解析的数据的格式,又应该如何来解析呢?
JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。
Go类型和JSON类型的对应关系如下:
- bool 代表 JSON booleans,
- float64 代表 JSON numbers,
- string 代表 JSON strings,
- nil 代表 JSON null.
目前bitly公司开源了一个叫做simplejson
的包,在处理未知结构体的JSON时相当方便
js, err := NewJson([]byte(`{
"test": {
"array": [1, "2", 3],
"int": 10,
"float": 5.150,
"bignum": 9223372036854775807,
"string": "simplejson",
"bool": true
}
}`))
arr, _ := js.Get("test").Get("array").Array()
i, _ := js.Get("test").Get("int").Int()
ms := js.Get("test").Get("string").MustString()
https://github.com/bitly/go-simplejson
高并发
Golang并发解决方案goroutine类比协程。协程在内存消耗和切换调度开销都远比线程小。
goroutine有以下特点
1)轻量级:创建和销毁非常快速,每个 goroutine 只需要几 KB 的内存,因此可同时创建数百万个
2)并发安全:goroutine 本质上是协作式调度的,而非抢占式调度,因此不会发生数据竞争问题
3)通信机制:goroutine 之间可通过通道(channel)进行通信,高效且安全,可以避免锁的使用
一个简单的高并发端口扫描示例
package main
import (
"fmt"
"net"
"sync"
"time"
)
// 定义主机地址结构体
type Addr struct {
ip string
port int
}
// 定义要扫描的IP地址和端口号
var (
ips = []string{"172.16.51.221", "172.16.51.18", "172.16.50.1", "172.16.50.4"}
ports = []int{80, 443, 445, 8080}
AliveHosts []string //存活服务结果存放数组
timeout int64 = 5 //连接超时时间
)
// 定义协程数量
const maxGoroutines = 50
// 定义等待组
var wg sync.WaitGroup
func main() {
// 初始化通道,必须使用make 创建channel通道
Addrs := make(chan Addr, len(ips)*len(ports))
results := make(chan string, len(ips)*len(ports))
//定义一个channel时,也需要定义发送到channel的值的类型。
//接收结果
go func() {
for res := range results {
fmt.Println(res)
wg.Done() //收到结果,计数器减一
}
}()
// 启动协程
for i := 0; i < maxGoroutines; i++ {
go func() {
for addr := range Addrs {
PortConnect(addr, results, timeout, &wg)
wg.Done() //完成一个任务,计数器减一
}
}()
}
// 向通道中添加任务(添加扫描目标)
for _, ip := range ips {
for _, port := range ports {
wg.Add(1) //添加一个任务,计数器加一
Addrs <- Addr{ip, port} //channel通过操作符<-来接收和发送数据
}
}
// 等待所有协程完成
wg.Wait()
// 关闭通道,必须在wg.Wait之后
close(Addrs)
close(results)
}
// TCP Connect-端口连接测试
func PortConnect(addr Addr, res chan<- string, timeout int64, wg *sync.WaitGroup) {
ip, port := addr.ip, addr.port
conn, err := net.DialTimeout("tcp4", fmt.Sprintf("%s:%v", ip, port), time.Duration(timeout)*time.Second)
//关闭网络连接
defer func() {
if conn != nil {
conn.Close()
}
}()
if err == nil {
//产出结果,计数器加一
wg.Add(1)
res <- fmt.Sprintf("%s:%v", ip, port)
}
}
推荐阅读下面的这篇文章来理解Golang的高并发
GMP 并发调度器深度解析之手撸一个高性能 goroutine pool
runtime包中有几个处理goroutine的函数:
Goexit
退出当前执行的goroutine,但是defer函数还会继续调用
Gosched
让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
NumCPU
返回 CPU 核数量
NumGoroutine
返回正在执行和排队的任务总数
GOMAXPROCS
用来设置可以并行计算的CPU核数的最大值,并返回之前的值。
异常处理
Golang通过panic抛出异常,然后在defer中,通过recover捕获异常并处理。
panic 的中文翻译为 恐慌,当程序遇到错误的时候就会恐慌造成程序崩溃!
defer
语句将一个函数推迟到当前函数返回前执行。
package main
import (
"errors"
"fmt"
"math"
)
func GetBallVolumn(radius float64) (vol float64, err error) {
if radius < 0 {
//直接panic程序不会崩溃,但是后边的程序语句不会再执行
panic("小球半径不能为负数(From GetBallVolumn Func)")
}
//一种温和的错误提示,而不是直接 panic 让程序崩溃掉
//定义错误信息,如果存在则直接return
if radius < 5 || radius > 50 {
err = errors.New("半径的合法范围是5~50")
return 0, err
}
return (4 / 3.0) * math.Pi * math.Pow(radius, 3), nil //没错误err返回nil
}
func main() {
//在函数结束之前处理异常
defer func() {
if err := recover(); err != nil {
fmt.Println("严重错误警告: ", err)
}
}()
volumn, err := GetBallVolumn(2)
//如果存在错误则提示错误信息
if err != nil {
fmt.Println("获取体积失败,err=", err)
}
fmt.Println("小球的体积是:", volumn)
fmt.Println("程序运行结束")
}
函数-穿指针
- 传指针使得多个函数能操作同一个对象。
- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
- Go语言中
channel
,slice
,map
这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice
的长度,则仍需要取地址传递指针)
//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
*a = *a+1 // 修改了a的值
return *a // 返回新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(&x) // 调用 add1(&x) 传x的地址
fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
fmt.Println("x = ", x) // 应该输出 "x = 4"
}
匿名函数和defer
匿名函数和defer语句不能乱用。
匿名函数
匿名函数是一种没有名称的函数,它可以在代码中直接定义和使用。
匿名函数可以在函数内部定义,也可以作为函数参数或返回值。
匿名函数的副作用主要有以下几点
1)可能会导致内存泄漏:匿名函数中使用外部变量时,会形成闭包,如果外部变量一直被引用,那么它所占用的内存就不会被释放,从而导致内存泄漏。
2)可能会导致并发问题:匿名函数中使用外部变量时,如果多个协程同时访问这个变量,可能会引发并发问题,如竞态条件等。
3)可能会影响代码可读性:过度使用匿名函数会使代码变得难以阅读和理解,从而降低代码的可维护性和可扩展性。
如何合理应用匿名函数
1)可以作为函数参数或返回值,这种方式可以使代码更加灵活和可扩展
2)可以作为协程的执行体,这种方式可以方便地实现并发编程
3)可以形成闭包,访问外部变量,从而实现一些有用的功能,如函数柯里化、延迟执行等
defer语句
defer
语句可以用于在函数返回时执行某个操作,无论函数是正常返回还是发生了异常。
过度使用defer语句的危害
当一个函数中存在大量的defer
语句时,这些语句会被添加到函数的调用栈中,并在函数返回时按照相反的顺序执行,这可能会导致函数的运行速度变慢,尤其是在函数被频繁调用的情况下。
defer使用场景
1)进行资源的释放:如文件句柄、网络连接、数据库连接等资源的释放。
2)锁的释放:如互斥锁、读写锁等锁的释放。
3)延迟执行某个操作:如对某个函数的延迟调用,或者对某个操作的延迟执行。
关于retrun
在函数没有返回值的情况下是没有必要的。
如果在函数中遇到了某些错误,你可能希望立即返回,而不是继续执行后面的代码。
在这种情况下,可以使用return
语句来提前结束函数的执行。
泛型
泛型编程是一种计算机编程风格,编程范式,其中算法是根据稍后指定的类型编写的,然后在需要时为作为参数提供的特定类型实例化。
泛型有以下特点
1)类型的参数化:把类型当做函数参数传递。
2)更强的类型检查:可以在编译期间对类型进行检查,减少运行时由于对象类型不匹配引发的异常
3)代码节省与抽象呈现
示例如下
package main
import "fmt"
// 声明类型约束(整型和浮点型)
type Number interface {
int64 | float64
}
// 通用型函数,自动判断参数的类型
// 在[]括号内,声明两个类型参数K和V,以及一个使用类型参数的参数m map[K]V
func SumIntsOrFloats[K comparable, V Number](m map[K]V) V {
var s V //声明的s变量可能是整型 也可能是浮点型
for _, v := range m {
s += v
}
return s
}
func main() {
// Initialize a map for the integer values
ints := map[string]int64{
"first": 34,
"second": 12,
}
// Initialize a map for the float values
floats := map[string]float64{
"first": 35.98,
"second": 26.99,
}
//引用泛型函数,自动判断参数类型
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats))
}