Go语言手册
(2023-6-14更新)
资料来源
Go语言标准库文档中文版
Go 语言教程 | runoob
Go语言学习 | rickiyang | cnblogs
保姆级go语言(golang)入门系列课程 | bilibli | 高性能golang
AI-GPT-4
基础知识
环境安装
(Ubuntu22.04 LTS)
sudo apt-get install && apt-get upgrade
sudo apt-get install golang
go version #安装后的版本查询
mkdir ~/workspace
echo 'export GOPATH="$HOME/workspace"' >> ~/.bashrc
source ~/.bashrc
结构
package main
import "fmt"
func main() {
/*注释符号*/
//花括号不能单独放行
fmt.Println("Hello, World!")
}
// 单行注释
go run helloworld.go
- 这个程序的第一行
package main
定义了这个文件属于 main 包。在 Go 语言中,每个程序都必须包含一个 main 包,并且在这个包下必须包含一个 main() 函数,作为程序的入口点。 - 第三行
import "fmt"
引入了 fmt 包,这个包提供了格式化输入输出的函数。 - 第五行
func main() {
定义了程序的入口点 main() 函数。 - 第六行
fmt.Println("Hello, World!")
调用了 fmt 包中的 Println() 函数,用于输出一条字符串 "Hello, World!" 到标准输出设备上(通常是控制台)。 - 最后一行
}
表示 main() 函数的结束。
Go 语言的基础组成有以下几个部分:
- 包声明
- 引入包
- 函数
- 变量
- 语句 & 表达式
- 注释
标识符实际上就是一个或是多个字母(AZ和az)数字(0~9)、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。
Go 语言的字符串连接可以通过 + 实现
Go 代码中会使用到的 25 个关键字或保留字:
break | default | func | interface | select |
---|---|---|---|---|
case | defer | go | map | struct |
chan | else | goto | package | switch |
const | fallthrough | if | range | type |
continue | for | import | return | var |
Go 语言还有 36 个预定义标识符:
append | bool | byte | cap | close | complex | complex64 | complex128 | uint16 |
---|---|---|---|---|---|---|---|---|
copy | false | float32 | float64 | imag | int | int8 | int16 | uint32 |
int32 | int64 | iota | len | make | new | nil | panic | uint64 |
println | real | recover | string | true | uint | uint8 | uintptr |
程序一般由关键字、常量、变量、运算符、类型和函数组成。
程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}。
程序中可能会使用到这些标点符号:.、,、;、: 和 …。
GO语言区分大小写
-
任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。
-
当命名(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Analysize,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);
-
命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )
-
包名应该为小写单词,不要使用下划线或者混合大小写。
文件命名
尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词。
结构体命名采用驼峰命名法,首字母根据访问控制大写或者小写
struct 申明和初始化格式采用多行
type MainConfig struct {
Port string `json:"port"`
Address string `json:"address"`
}
config := MainConfig{"1234", "123.221.134"}
接口命名
命名规则基本和上面的结构体类型
单个函数的结构名以 “er” 作为后缀,例如 Reader , Writer 。
type Reader interface {
Read(p []byte) (n int, err error)
}
变量命名
和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:
- 如果变量为私有,且特有名词为首个单词,则使用小写,如 appService
- 若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
常量均需使用全部大写字母组成,并使用下划线分词
const APP_URL = "https://www.baidu.com"
如果是枚举类型的常量,需要先创建相应类型:
type Scheme string
const (
HTTP Scheme = "http"
HTTPS Scheme = "https"
)
格式化字符串
Go 语言中使用 fmt.Sprintf 或 fmt.Printf 格式化字符串并赋值给新串:
- Sprintf 根据格式化参数生成格式化的字符串并返回该字符串。
- Printf 根据格式化参数生成格式化的字符串并写入标准输出。
// 当前程序的包名
package main
// 导入其他包
import . "fmt"
// 常量定义
const PI = 3.14
// 全局变量的声明和赋值
var name = "gopher"
// 一般类型声明
type newType int
// 结构的声明
type gopher struct{}
// 接口的声明
type golang interface{}
// 由main函数作为程序入口点启动
func main() {
Println("Hello World!")
}
错误处理
- 错误处理的原则就是不能丢弃任何有返回err的调用,不要使用 _ 丢弃,必须全部处理。接收到错误,要么返回err,或者使用log记录下来
- 尽早return:一旦有错误发生,马上返回
- 尽量不要使用panic,除非你知道你在做什么
- 错误描述如果是英文必须为小写,不需要标点结尾
- 采用独立的错误流进行处理
// 错误写法
if err != nil {
// error handling
} else {
// normal code
}
// 正确写法
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
常量
在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。
const PI = 3.14
变量
//定义一个名称为“valName”,类型为"type"的变量
var valName type
//定义三个类型都是“type”的变量
var vname1, vname2, vname3 type
//定义三个类型都是"type"的变量,并且分别初始化为相应的值
//vname1为v1,vname2为v2,vname3为v3
var vname1, vname2, vname3 type= v1, v2, v3
var vname1, vname2, vname3 = v1, v2, v3
vname1, vname2, vname3 := v1, v2, v3
/*
:=这个符号直接取代了var和type,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;
在函数外部使用则会无法编译通过,所以一般用var方式来定义全局变量。
*/
//_(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值2赋予b,并同时丢弃1:
_, b := 1, 2
内置数据类型
Go 语言按类别有以下几种数据类型:
布尔型 --> 在Go中,布尔值的类型为bool
,值只可以是常量 true
或者 false
。一个简单的例子:var b bool = true
;
整数型 --> 整型 int
和浮点型 float
,Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码;
字符串 --> 字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本;
派生型 -->
(a) 指针类型(Pointer)
(b) 数组类型
(c) 结构化类型(struct)
(d) 联合体类型 (union)
(e) 函数类型
(f) 切片类型
(g) 接口类型(interface)
(h) Map 类型
(i) Channel 类型
//Boolean
var isActive bool // 全局变量声明
var enabled, disabled = true, false // 忽略类型的声明
func test() {
var available bool // 一般声明
valid := false // 简短声明
available = true // 赋值操作
}
uint8 | 无符号 8 位整型 (0 到 255) |
---|---|
uint16 | 无符号 16 位整型 (0 到 65535) |
uint32 | 无符号 32 位整型 (0 到 4294967295) |
uint64 | 无符号 64 位整型 (0 到 18446744073709551615) |
int8 | 有符号 8 位整型 (-128 到 127) |
int16 | 有符号 16 位整型 (-32768 到 32767) |
int32 | 有符号 32 位整型 (-2147483648 到 2147483647) |
int64 | 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
float32 | IEEE-754 32位浮点型数 |
float64 | IEEE-754 64位浮点型数 |
complex64 | 32 位实数和虚数 |
complex128 | 64 位实数和虚数 |
byte | 类似 uint8 |
rune | 类似 int32 |
uint | 32 或 64 位 |
int | 与 uint 一样大小 |
uintptr | 无符号整型,用于存放一个指针 |
字符串
Go中的字符串都是采用UTF-8字符集编码,字符串是用一对双引号("")或反引号()括起来定义,它的类型是 string。
func test(a,b int) {
str := "hello world"
m := "haha"
result := str + m
println(result)
multStr := `hello
world`
println(multStr)
}
- \n:换行
- \r:回车
- \t:tab键
- \u:Unicode字符
- \f:换页
- \v:垂直制表符
- \b:退格
格式 | 输出 |
---|---|
%v | 按值的本来值输出 |
%+v | 在 %v 基础上,对结构体字段名和值进行展开 |
%#v | 输出 Go 语言语法格式的值 |
%T | 输出 Go 语言语法格式的类型和值 |
%% | 输出 % 本体 |
%c | 输出单个字符 |
%s | 输出字符串 |
%b | 整型以二进制方式显示 |
%o | 整型以八进制方式显示 |
%d | 整型以十进制方式显示 |
%x | 整型以十六进制方式显示 |
%X | 整型以十六进制、字母大写方式显示 |
%U | Unicode 字符 |
%f | 浮点数 |
%p | 指针,十六进制方式显示 |
字符串的定义和基本操作
package main
import "fmt"
func main() {
// 定义一个字符串变量
var str string = "Hello, World!"
// 输出字符串
fmt.Println(str)
// 计算字符串长度
length := len(str)
fmt.Println("Length:", length)
// 字符串拼接
str1 := "Hello"
str2 := "World"
str3 := str1 + " " + str2
fmt.Println(str3)
// 字符串分割
str4 := "a,b,c,d"
parts := strings.Split(str4, ",")
fmt.Println(parts)
}
/*
Hello,World!
Length: 12
Hello World
[a b c d]
*/
字符串的遍历和修改
[]byte
表示字节切片类型,将 str2
转换为字节切片类型 bytes
,然后将其中的第一个字节修改为小写字母 h
,最后将 bytes
转换为字符串类型 str3
并打印出来。
(可以理解为将每个字符分开,选择位置然后修改字符)
package main
import "fmt"
func main() {
// 定义一个字符串变量
str := "Hello, World!"
// 遍历字符串
for i := 0; i < len(str); i++ {
fmt.Printf("%c\n", str[i])
}
// 通过切片修改字符串
str2 := "Hello, World!"
bytes := []byte(str2)
bytes[7] = 'G'
str3 := string(bytes)
fmt.Println(str3)
}
/*
H
e
l
l
o
,
W
o
r
l
d
!
Hello,WGrld!
*/
字符串的格式化输出
package main
import "fmt"
func main() {
// 定义一个字符串变量
str := "Hello, World!"
// 使用 fmt.Printf 格式化输出字符串
fmt.Printf("String: %s\n", str)
// 使用 fmt.Printf 格式化输出字符串长度
fmt.Printf("Length: %d\n", len(str))
// 使用 fmt.Printf 格式化输出字符串中某个字符的 ASCII 码值
fmt.Printf("ASCII: %d\n", str[0])
}
/*
String: Hello, World!
Length: 13
ASCII: 72
*/
字符串比较
在 Go 语言中,可以使用 ==
和 !=
运算符来比较两个字符串是否相等。例如:
package main
import "fmt"
func main() {
str1 := "Hello, World!"
str2 := "Hello, Go!"
if str1 == str2 {
fmt.Println("Equal")
} else {
fmt.Println("Not equal")
}
}
//Not equal
字符串搜索
在 Go 语言中,可以使用 strings
包提供的各种函数来搜索字符串。例如,strings.Contains()
函数可以判断一个字符串是否包含另一个字符串,strings.Index()
函数可以查找一个字符串在另一个字符串中第一次出现的位置,strings.LastIndex()
函数可以查找一个字符串在另一个字符串中最后一次出现的位置。例如:
package main
import (
"fmt"
"strings"
)
func main() {
str := "Hello,World!"
if strings.Contains(str, "World") {
fmt.Println("Found")
} else {
fmt.Println("Not found")
}
pos := strings.Index(str, "o")
fmt.Println("First index of 'o':", pos) //找到‘o’这个字符第一次出现
pos = strings.LastIndex(str, "o")
fmt.Println("Last index of 'o':", pos) //最后一次出现
}
/*
Found
First index of 'o': 4
Last index of 'o': 7
*/
运算符
优先级 | 运算符 |
---|---|
5 | * / % << >> & &^ |
4 | + - | ^ |
3 | == != < <= > >= |
2 | && |
1 | || |
条件语句
Go 语言中的条件语句主要包括 if
、switch
和 select
三种语句。
if 语句
if 语句的基本语法如下:
if condition {
// 如果条件成立,执行这里的代码块
}
其中,condition
表示一个布尔表达式,如果 condition
为真,则执行 if
后面的代码块,否则不执行。
package main
import "fmt"
func main() {
num := 10
if num < 20 {
fmt.Println("num is less than 20")
}
}
在上面的代码中,我们使用 if
语句判断变量 num
是否小于 20,如果成立,则打印出相应的信息。
除了基本的 if
语句外,Go 语言还支持在条件语句前面添加一个简短的语句,用于初始化变量。例如:
package main
import "fmt"
func main() {
if num := 10; num < 20 {
fmt.Println("num is less than 20")
}
}
在上面的代码中,我们在 if
语句前面添加了一个简短的语句 num := 10
,用于初始化变量 num
的值。然后我们判断 num
是否小于 20,如果成立,则打印出相应的信息。
if condition {
// 如果条件成立,执行这里的代码块
} else {
// 如果条件不成立,执行这里的代码块
}
condition
是一个布尔表达式,如果为真,则执行if语句后面的代码块,否则执行else后面的代码块。可以看出,if...else语句只有两种情况,要么执行if代码块,要么执行else代码块。
除了基本的if...else语句外,Go语言还支持if...else if...else语句,用于根据不同的条件执行不同的代码块。if...else if...else语句的基本语法如下:
if condition1 {
// 如果条件1成立,执行这里的代码块
} else if condition2 {
// 如果条件1不成立,且条件2成立,执行这里的代码块
} else {
// 如果条件1和条件2都不成立,执行这里的代码块
}
condition1
和 condition2
都是布尔表达式,如果 condition1
为真,则执行第一个代码块;如果 condition1
不为真,且 condition2
为真,则执行第二个代码块;否则执行第三个代码块。
除了if...else语句和if...else if...else语句外,Go语言还支持if嵌套语句,用于根据多个条件执行不同的代码块。if嵌套语句的基本语法如下:
if condition1 {
// 如果条件1成立,执行这里的代码块
if condition2 {
// 如果条件1和条件2都成立,执行这里的代码块
}
} else {
// 如果条件1不成立,执行这里的代码块
}
在上面的代码中,我们使用了一层嵌套的if语句,根据两个条件来执行不同的代码块。首先判断 condition1
是否成立,如果成立,则继续判断 condition2
是否成立,如果成立,则执行第二个代码块;否则执行第一个代码块。如果 condition1
不成立,则执行else后面的代码块。
switch 语句
switch 语句的基本语法如下:
switch expression {
case value1:
// 如果 expression 的值等于 value1,执行这里的代码块
case value2:
// 如果 expression 的值等于 value2,执行这里的代码块
default:
// 如果 expression 的值都不等于上面的值,执行这里的代码块
}
其中,expression
表示一个表达式,可以是任何类型,而 value1
、value2
等则表示具体的值。当 expression
的值等于某个 case
后面的值时,就会执行相应的代码块。如果 expression
的值都不等于上面的值,则执行 default
后面的代码块。
package main
import "fmt"
func main() {
num := 3
switch num {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
default:
fmt.Println("other")
}
}
在上面的代码中,我们使用 switch
语句判断变量 num
的值,并根据不同的值执行不同的代码块。
与 if
语句类似,switch
语句也支持在条件语句前面添加一个简短的语句,用于初始化变量。例如:
package main
import "fmt"
func main() {
switch num := 3; num {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
default:
fmt.Println("other")
}
}
在上面的代码中,我们在 switch
语句前面添加了一个简短的语句 num := 3
,用于初始化变量 num
的值。
select 语句
select 语句用于处理通道(Channel)的发送和接收操作。其基本语法如下:
select {
case msg1 := <-channel1:
// 从 channel1 接收数据,并将数据赋值给变量 msg1
// 如果 channel1 没有数据可接收,则阻塞在这里
case channel2 <- msg2:
// 向 channel2 发送数据 msg2
// 如果 channel2 没有空间可发送,则阻塞在这里
default:
// 如果所有的 case 都没有匹配到,则执行这里的代码块
}
其中,channel1
和 channel2
表示通道变量,msg1
和 msg2
表示通道中的数据。当 select
语句执行时,会从多个通道中选择一个有数据可读或者有空间可写的通道执行相应的操作。如果所有的通道都没有数据可读或者没有空间可写,则执行 default
后面的代码块。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan int)
go func() {
ch1 <- "hello"
}()
go func() {
ch2 <- 10
}()
select {
case str := <-ch1:
fmt.Println(str)
case num := <-ch2:
fmt.Println(num)
}
}
在上面的代码中,我们定义了两个通道 ch1
和 ch2
,并将字符串 "hello"
和整数 10
分别发送到这两个通道中。然后我们使用 select
语句从多个通道中选择一个有数据可读的通道,并打印出其结果。由于通道中的数据是异步发送和接收的,因此输出的结果可能是字符串 "hello"
或者整数 10
中的任意一个。
循环语句
Go语言中的循环语句用于重复执行一定的代码块,常用的循环语句有for循环、循环嵌套、break语句、continue语句和goto语句。
for循环
for循环是Go语言中最基本的循环语句,用于重复执行一定的代码块。它的基本语法如下:
for 初始化语句; 条件表达式; 后置语句 {
// 循环体
}
其中,初始化语句用于初始化循环变量,条件表达式用于判断是否继续执行循环,后置语句用于更新循环变量。循环体是需要重复执行的代码块。
循环嵌套
循环嵌套是指在一个循环语句中嵌套另一个循环语句,以实现更复杂的循环逻辑。例如,下面的代码展示了一个简单的循环嵌套:
for i := 0; i < 10; i++ {
for j := 0; j < 5; j++ {
fmt.Print(i * j, " ")
}
fmt.Println()
}
/*
0 0 0 0 0
0 1 2 3 4
0 2 4 6 8
0 3 6 9 12
0 4 8 12 16
0 5 10 15 20
0 6 12 18 24
0 7 14 21 28
0 8 16 24 32
0 9 18 27 36
*/
在上面的代码中,我们使用了两个for循环,一个是外层的循环,一个是内层的循环。内层的循环会在每次外层循环执行时重复执行,以实现更复杂的循环逻辑。
break语句
break语句用于跳出循环,即在循环体中使用break语句会立即退出循环。例如,下面的代码展示了如何使用break语句:
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Print(i, " ")
}
//0 1 2 3 4
在上面的代码中,当i等于5时,使用break语句跳出循环,不再执行后面的代码。
continue语句
continue语句用于跳过循环中的某一次迭代,即在循环体中使用continue语句会立即跳过本次循环,进入下一次循环。例如,下面的代码展示了如何使用continue语句:
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Print(i, " ")
}
//1 3 5 7 9
在上面的代码中,当i是偶数时,使用continue语句跳过本次循环,不再执行后面的代码。
goto语句
goto语句用于无条件跳转到指定的标签,即在循环体中使用goto语句可以跳转到指定的标签处执行代码。例如,下面的代码展示了如何使用goto语句:
for i := 0; i < 10; i++ {
if i == 5 {
goto LABEL
}
fmt.Print(i, " ")
}
LABEL:
fmt.Println("Jumped to label")
在上面的代码中,当i等于5时,使用goto语句跳转到标签LABEL处执行代码。注意,使用goto语句会增加代码的复杂度和阅读难度,因此应该尽量避免使用。
切片
Go语言中的切片是一种灵活、动态的数据结构,它与数组类似,但长度是可变的。切片函数是用于操作切片的一系列内置函数,包括len()、cap()、nil、append()、copy()
等等。
len()函数
len()函数用于获取切片的长度,即切片中元素的个数。例如,下面的代码展示了如何使用len()函数获取切片的长度:
s := []int{1, 2, 3, 4, 5}
fmt.Println(len(s)) // 输出:5
这是一个包含5个整数的切片,切片名为s,元素为1、2、3、4、5。可以通过下标访问切片中的元素,例如s[0]表示获取第一个元素,即1。
在上面的代码中,使用len()函数获取切片s的长度,即5。
cap()函数
cap()函数用于获取切片的容量,即切片可以容纳的元素个数。例如,下面的代码展示了如何使用cap()函数获取切片的容量:
s := make([]int, 5, 10)
fmt.Println(cap(s)) // 输出:10
在上面的代码中,使用make()函数创建一个切片,长度为5,容量为10,使用cap()函数获取切片s的容量,即10。
容量表示切片底层数组的长度,长度表示切片中元素的个数。
(make函数,像是规定一个盒子,盒子大小是能放10个东西,长度是你放了5个东西在能放10个东西的盒子里面)
nil切片
nil切片是指没有分配任何数据空间的切片,它的长度和容量为0。在Go语言中,切片的零值就是nil切片。例如,下面的代码展示了如何创建一个nil切片:
var s []int
fmt.Println(s == nil) // 输出:true
在上面的代码中,定义一个变量s,它的类型为[]int,由于没有分配任何数据空间,s就是一个nil切片。
append()函数
append()函数用于向切片中追加元素,可以一次追加一个或多个元素。如果切片容量不足以容纳新元素,则会自动扩容。例如,下面的代码展示了如何使用append()函数向切片中追加元素:
s := []int{1, 2, 3}
s = append(s, 4, 5, 6)
fmt.Println(s) // 输出:[1 2 3 4 5 6]
在上面的代码中,定义一个切片s,包含元素1、2、3,使用append()函数向切片s中追加元素4、5、6,最终s包含元素1、2、3、4、5、6。
copy()函数
copy()函数用于将一个切片的内容复制到另一个切片中。例如,下面的代码展示了如何使用copy()函数复制切片:
s1 := []int{1, 2, 3}
s2 := make([]int, len(s1))
copy(s2, s1)
fmt.Println(s2) // 输出:[1 2 3]
在上面的代码中,定义一个切片s1,包含元素1、2、3,使用make()函数创建一个长度与s1相同的切片s2,使用copy()函数将s1中的内容复制到s2中,最终s2包含元素1、2、3。
map集合
Go语言中的map是一种无序的键值对集合,其中每个键唯一对应一个值。map中的所有键和所有值的类型必须相同,可以是任何内置或自定义类型。下面是一个简单的例子,代码中注释有详细说明:
package main
import "fmt"
func main() {
// 创建一个空的map,键为string类型,值为int类型
m1 := make(map[string]int)
// 向map中添加键值对
m1["a"] = 1
m1["b"] = 2
m1["c"] = 3
// 获取map中指定键的值
fmt.Println("m1[a] =", m1["a"]) // 输出 m1[a] = 1
// 获取map的长度
fmt.Println("len(m1) =", len(m1)) // 输出 len(m1) = 3
// 判断map中是否存在指定键
val, ok := m1["d"]
if ok {
fmt.Println("m1[d] =", val)
} else {
fmt.Println("m1[d] does not exist")
}//m1[d] does not exist
// 删除map中的指定键值对
delete(m1, "c")
// 遍历map中的键值对
for k, v := range m1 {
fmt.Println(k, v)
}
/*
a 1
b 2
*/
}
这个例子中,我们首先创建了一个空的map,键为string类型,值为int类型。
然后我们向map中添加了三个键值对,分别是"a":1、"b":2和"c":3。我们可以通过指定键来获取map中的值,例如m1["a"]返回1。我们可以使用len()函数获取map的长度,例如len(m1)返回3。
我们可以通过判断第二个返回值来判断map中是否存在指定的键,例如val, ok := m1["d"],如果键"d"不存在,ok将为false,否则将为true,并且val将为键"d"对应的值。
我们可以使用delete()函数删除map中的指定键值对,例如delete(m1, "c")将删除键"c"对应的值3。
最后我们使用for循环遍历map中的所有键值对,并输出它们的键和值。
(可以看作SQL里面的建表,create table test ( string int );,insert into test (string,int) values (a,1))
(ok := 就是判断是否有这个值)
函数
Go语言中的函数是一种可重用的代码块,用于封装特定的功能。函数定义格式如下:
func 函数名(参数列表) 返回值类型 {
函数体
}
其中,参数列表可以为空,也可以包含多个参数,每个参数包含名称和类型。返回值类型可以是单个类型或多个类型组成的元组。函数体中实现了函数的具体功能,并通过return语句返回结果。
下面是一些常见的函数用法:
函数调用
函数调用格式如下:
函数名(参数列表)
参数列表中需要传递与函数定义中参数列表相同类型和数量的参数。例如:
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
res := add(1, 2)
fmt.Println(res) // 输出 3
}
返回多值
函数可以返回多个值,这些值可以是不同类型的。例如:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
func main() {
res, err := divide(10, 2)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(res) // 输出 5
}
}
这个例子中,我们定义了一个名为divide的函数,该函数接收两个float64类型的参数a和b,并返回两个值:a/b和一个error类型的值(用于处理除数为0的情况)。在主函数中,我们调用了divide函数,并使用res和err两个变量分别接收其返回值。如果err不为nil,则说明除数为0,需要进行错误处理;否则,我们可以使用res变量来获取计算结果。
函数参数
函数可以接收多种参数类型,包括:
- 值类型:函数接收的参数是值的副本,对参数的修改不会影响原始值;
- 指针类型:函数接收的参数是指向值的指针,对参数的修改会影响原始值;
- 可变参数:函数接收的参数数量是可变的,可以接收任意数量的参数。
例如:
func changeValue(a int) {
a = 10
}
func changePointer(a *int) {
*a = 10
}
func sum(nums ...int) int {
res := 0
for _, num := range nums {
res += num
}
return res
}
func main() {
num := 5
changeValue(num)
fmt.Println(num) // 输出 5
changePointer(&num)
fmt.Println(num) // 输出 10
res := sum(1, 2, 3, 4, 5)
fmt.Println(res) // 输出 15
}
这个例子中,我们定义了三个函数changeValue、changePointer和sum,分别演示了值类型、指针类型和可变参数的用法。在主函数中,我们先定义了一个变量num,然后分别调用了changeValue和changePointer函数,分别修改了其值。最后,我们调用了sum函数,传递了5个参数,并将结果赋给了res变量。
(接收一个可变参数nums,类型为int类型的切片。使用for循环遍历nums中的每个元素,并将它们加起来,最后返回总和。range遍历nums切片中的每个元素,通过_忽略了每个元素的索引,只保留了元素本身。
...int表示可变参数列表,也称为不定参数。这意味着函数可以接受任意数量的int类型参数,并将它们视为一个int类型的切片。)
func sum(nums ...int) int { // 定义一个名为sum的函数,参数为int类型的可变参数nums,返回值为int类型
res := 0 // 定义一个变量res,初始值为0
for i := range nums { // 使用for循环遍历nums切片中的每个元素,其中i表示当前元素的索引,num表示当前元素的值
res += nums[i] // 将当前元素加到变量res中
}
return res // 返回变量res的值,表示所有元素的和
}
func main() {
res := sum(1, 2, 3, 4, 5) // 调用sum函数,传递5个int类型的参数,并将返回值赋给res变量
fmt.Println(res) // 在控制台输出res的值,即所有参数的和
}
在这个例子中,我们将循环改为使用变量i表示当前元素的索引,这样可以访问nums切片中的任意元素。在循环体中,我们使用nums[i]访问当前元素的值,并将其加到变量res中。
需要注意的是,在这种情况下,我们不能使用_来忽略索引,因为我们需要访问每个元素的值和索引。因此,我们需要显式地将索引变量命名为i或其他名称。
这个函数仍然接受任意数量的int类型参数,并将它们视为一个int类型的切片。
defer
defer是Go语言的一个关键字,它可以让我们在函数执行完毕之后再执行一些特定的操作。无论函数是通过return正常返回,还是触发了panic异常,defer语句都能够确保在函数退出前被执行。
下面是一个简单的例子,演示defer语句的使用:
func main() {
defer fmt.Println("deferred statement")
fmt.Println("hello")
}
在这个例子中,我们在main函数中使用了defer语句,将一条语句fmt.Println("deferred statement")推迟到函数返回前执行。在函数体中,我们先输出了一条hello语句,然后函数执行完毕,defer语句被执行,输出了deferred statement。最终,程序退出。
defer语句的执行顺序是后进先出,也就是说,最后一个defer语句会最先执行,而第一个defer语句会最后执行。例如:
func main() {
defer fmt.Println("deferred statement 1")
defer fmt.Println("deferred statement 2")
defer fmt.Println("deferred statement 3")
fmt.Println("hello")
}
在这个例子中,我们使用了三个defer语句,分别输出了三条deferred statement语句。在函数体中,我们先输出了一条hello语句。最终,程序退出时,defer语句会按照后进先出的顺序执行,先输出deferred statement 3,然后是deferred statement 2,最后是deferred statement 1。
func foo() (string, int) {
a, b := 3, 5
c := a + b
defer fmt.Println("deferred statement 1", c)
defer fmt.Println("deferred statement 2", c)
defer func() {
defer fmt.Println("deferred statement 3", c)
}()
c = 100
fmt.Println("hello")
return "result:", c
}
func main(){
foo()
}
/*
hello
deferred statement 3 100
deferred statement 2 8
deferred statement 1 8
*/
defer语句中的变量c的值是在调用defer语句时确定的,而不是在执行defer语句时确定的。因此,第一个和第二个defer语句中输出的变量c的值都是8,而第三个defer语句中输出的变量c的值是100。这是因为在第三个defer语句中,我们使用了一个匿名函数,延迟了defer语句的执行,使得变量c的值在执行时已经被修改为了100。
接口
Go语言中的接口(interface)是一种类型,它定义了一组方法的集合。实现这些方法的任何类型都可以被称为这个接口的实现类型。接口的定义和使用能够大大提高代码的灵活性和可复用性。
定义接口
在Go语言中,定义接口非常简单,只需要使用interface关键字即可。例如,我们可以定义一个名为Animal的接口,它包含两个方法:Eat()和Sleep()。
type Animal interface {
Eat()
Sleep()
}
在这个例子中,我们定义了一个名为Animal的接口,它包含了两个方法:Eat()和Sleep()。任何实现了这两个方法的类型都可以被称为Animal接口的实现类型。
定义结构体
在Go语言中,结构体(struct)是一种自定义的数据类型,它可以包含多个字段,每个字段都有自己的类型和值。结构体的定义和使用也非常简单。例如,我们可以定义一个名为Dog的结构体,它包含了两个字段:Name和Age。
type Dog struct {
Name string
Age int
}
在这个例子中,我们定义了一个名为Dog的结构体,它包含了两个字段:Name和Age。这个结构体可以用来表示一只狗的信息,包括它的名字和年龄。
接口的实现
在Go语言中,要实现一个接口,只需要实现这个接口中定义的所有方法即可。例如,我们可以定义一个名为Poodle的类型,它实现了Animal接口中的两个方法:Eat()和Sleep()。
type Poodle struct {
Name string
Age int
}
func (p *Poodle) Eat() {
fmt.Printf("%s is eating.\n", p.Name)
}
func (p *Poodle) Sleep() {
fmt.Printf("%s is sleeping.\n", p.Name)
}
在这个例子中,我们定义了一个名为Poodle的类型,它包含了两个字段:Name和Age。同时,我们实现了Animal接口中的两个方法:Eat()和Sleep()。在Eat()方法中,我们输出了一条狗正在吃的信息;在Sleep()方法中,我们输出了一条狗正在睡觉的信息。
使用接口
在Go语言中,使用接口非常简单,只需要将实现了接口的类型赋值给接口变量即可。例如,我们可以创建一个名为animal的Animal接口变量,并将一个Poodle类型的值赋值给它。
func main() {
var animal Animal
poodle := &Poodle{Name: "Fido", Age: 2}
animal = poodle
animal.Eat()
animal.Sleep()
}
在这个例子中,我们首先创建了一个名为animal的Animal接口变量。然后,我们创建了一个Poodle类型的值,并将它赋值给animal。最后,我们在animal变量上调用了接口中定义的两个方法:Eat()和Sleep()。这两个方法实际上是Poodle类型中定义的方法,但是由于Poodle类型实现了Animal接口,因此我们可以将它赋值给Animal接口变量,并在Animal接口上调用这两个方法。
我理解的例子
(网上给的例子我实在看不懂,我只能通过我学过的东西来定义)
这里假设我要用iostat命令去查看磁盘占用率。
Device tps kB_read/s kB_wrtn/s kB_dscd/s %util
sda 0.10 0.00 3.87 0.00 0.00
sdb 0.00 0.00 0.00 0.00 0.00
在shell里面,使用的命令是
iostat -d -x -k | grep 'sda' | awk '{print $1,$NF}'
用-d和-x显示,grep过滤到sda,再awk格式化文本输出Device和util
用Go语言接口来体现就是如下
package main
import (
"fmt"
"os/exec"
"strings"
)
type iostat interface {
GetDevice() string
GetUtil() float64
}
type script struct {
name string
util float64
}
func (s *script) GetDevice() string {
return s.name
}
func (s *script) GetUtil() float64 {
return s.util
}
func main() {
// 执行 iostat -d -x -k | grep 'sda' | awk '{print $1,$NF}' 命令
cmd := exec.Command("sh", "-c", "iostat -d -x -k | grep 'sda' | awk '{print $1,$NF}'")
//输出的结果为sda 0.00
output, err := cmd.Output()
if err != nil {
fmt.Println("Error:", err)
return
}
// 解析 awk 命令的输出
fields := strings.Fields(string(output)) //用strings.Fields()函数切片输出结果
if len(fields) != 2 {
fmt.Println("Error: unexpected output format")
return
}
s := &script{name: fields[0]}
fmt.Sscanf(fields[1], "%f", &s.util) //占用率为浮点型
// 输出结果
fmt.Println(s.GetDevice())
fmt.Println(s.GetUtil())
}
func Fields(str string) []string
str子字符串的切片或者如果str仅包含空格,则返回空切片。
fmt.Sscanf(fields[1], "%f", &s.util)
占用率是浮点型,不能用fmt.Printf。
我的理解
将一个输出结果,用结构体(script)连接进入接口(iostat)将里面的信息提取出来(Device和%util),然后输出结果。
(用生活化的例子来说。你买了一杯珍珠奶茶,珍珠奶茶里面有珍珠(信息,Device和%util),奶茶顶上有塑封的纸(接口,iostat)。你通过吸管(结构体,script)插进你的纸,然后把珍珠吸进你的口腔里,然后吃掉(输出结果)。)
时间函数
Go 语言中的 time
标准库提供了一系列操作时间和日期的函数和类型。它可以用于获取当前时间、解析时间字符串、格式化时间、计算时间差等操作。
下面是一些常用的 time
标准库函数:
获取当前时间
time.Now()
函数可以获取当前本地时间。它返回一个 time.Time
类型的值,包含了当前的年、月、日、时、分、秒、纳秒和时区等信息。
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
fmt.Println(t) // 2023-06-13 07:00:59.688899896 +0000 UTC m=+0.000066904
}
解析时间字符串
time.Parse(layout, value)
函数可以将字符串解析为时间类型。其中 layout
参数指定了输入字符串的格式,value
参数表示输入字符串。如果解析成功,函数返回一个 time.Time
类型的值,否则返回一个错误。
package main
import (
"fmt"
"time"
)
func main() {
layout := "2006-01-02 15:04:05"
value := "2022-10-01 12:34:56"
t, err := time.Parse(layout, value)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(t) // 输出格式为 "2022-10-01 12:34:56 +0000 UTC"
}
(格式化时间字符串,然后返回输出结果)
格式化时间
time.Format(layout)
函数可以将时间类型格式化为字符串。其中 layout
参数指定了输出字符串的格式。如果时间类型的值不合法,函数会返回一个错误。
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
layout := "2006-01-02 15:04:05"
s := t.Format(layout)
fmt.Println(s) // 输出格式为 "2022-08-31 19:46:38"
}
在 Go 语言中,格式化时间字符串需要使用特定的日期和时间格式,例如“2006-01-02 15:04:05”。这是因为这个字符串正好包含了日期、时间和时区等所有信息,且不会与其他日期格式混淆。
计算时间差
time.Since(t)
函数可以计算当前时间与指定时间之间的时间差。它返回一个 time.Duration
类型的值,表示两个时间之间的纳秒数差值。如果指定的时间在当前时间之后,函数会返回一个负数。
package main
import (
"fmt"
"time"
)
func main() {
t1 := time.Now()
time.Sleep(2 * time.Second)
t2 := time.Now()
duration := t2.Sub(t1)
fmt.Println(duration) // 输出格式为 "2.0008635s"
}
上述代码中,time.Sleep(2 * time.Second)
让程序暂停 2 秒钟,然后计算 t1
和 t2
之间的时间差,输出的结果为 2.0008635s
。
func (t Time) Sub(u Time) Duration
返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。要获取时间点t-d(d为Duration),可以使用t.Add(-d)。
time.Duration
time.Duration
表示时间间隔,常用于计算时间差。它的类型为 int64
,表示纳秒数。可以使用常量 time.Second
、time.Minute
、time.Hour
等表示不同的时间间隔。例如,下面的代码创建了一个表示 10 秒钟的时间间隔:
d := 10 * time.Second
time.ParseDuration
time.ParseDuration()
函数可以将字符串解析为时间间隔。例如,下面的代码将字符串 "2h30m"
解析为表示 2 小时 30 分钟的时间间隔:
d, err := time.ParseDuration("2h30m")
if err != nil {
log.Fatal(err)
}
fmt.Println(d) // 输出 "2h30m0s"
time.Tick
time.Tick()
函数返回一个通道,每隔一段时间发送一个时间值。例如,下面的代码每隔 1 秒钟就输出一次当前时间:
for t := range time.Tick(time.Second) {
fmt.Println(t)
}
time.Sleep
time.Sleep()
函数可以让当前的 Goroutine 暂停一段时间。例如,下面的代码让程序暂停 1 秒钟,然后再输出一条消息:
time.Sleep(time.Second)
fmt.Println("Hello, world!")
time.After
time.After()
函数返回一个通道,在指定的时间后发送一个时间值。例如,下面的代码创建了一个通道,在 1 秒钟后发送一个时间值:
select {
case <-time.After(time.Second):
fmt.Println("Time's up!")
}
time.Timer
time.Timer
类型表示一个定时器,可以在指定的时间后执行某个操作。例如,下面的代码创建了一个定时器,在 1 秒钟后输出一条消息:
timer := time.NewTimer(time.Second)
<-timer.C
fmt.Println("Time's up!")
os读写文件
os
标准库提供了操作操作系统相关功能的函数和类型。包括文件和目录操作、环境变量、用户和组信息、进程管理等。下面介绍一些常用的函数和类型。
文件和目录操作
os.Create(filename string) (*os.File, error)
:创建一个新文件,如果文件已经存在则截断它。os.Open(filename string) (*os.File, error)
:打开一个文件进行读取操作。os.OpenFile(name string, flag int, perm FileMode) (*File, error)
:根据指定的标志打开文件。os.Remove(name string) error
:删除文件或空目录。os.RemoveAll(path string) error
:递归删除目录及其子目录。os.Rename(oldpath, newpath string) error
:重命名文件或移动文件到新路径。
下面是读取文件和写入文件的示例代码:
package main
import (
"fmt"
"io/ioutil" // 导入 io/ioutil 包,用于读写文件
"os" // 导入 os 包,用于操作系统相关功能
)
func main() {
// 写文件
content := "Hello, world!" // 创建文件内容
err := ioutil.WriteFile("test.txt", []byte(content), 0644) // 调用 ioutil.WriteFile 函数写入文件
if err != nil { // 如果写入失败,输出错误信息并退出程序
fmt.Println("写文件失败:", err)
return
}
fmt.Println("写文件成功!") // 如果写入成功,输出成功信息
// 读文件
data, err := ioutil.ReadFile("test.txt") // 调用 ioutil.ReadFile 函数读取文件内容
if err != nil { // 如果读取失败,输出错误信息并退出程序
fmt.Println("读文件失败:", err)
return
}
fmt.Println("文件内容:", string(data)) // 如果读取成功,输出文件内容
// 打开文件进行读取
file, err := os.Open("test.txt") // 调用 os.Open 函数打开文件
if err != nil { // 如果打开失败,输出错误信息并退出程序
fmt.Println("打开文件失败:", err)
return
}
defer file.Close() // 延迟关闭文件句柄
buf := make([]byte, 1024) // 创建一个 1024 字节的缓冲区
n, err := file.Read(buf) // 调用 file.Read 方法读取文件内容到缓冲区
if err != nil { // 如果读取失败,输出错误信息并退出程序
fmt.Println("读取文件失败:", err)
return
}
fmt.Println("文件内容:", string(buf[:n])) // 如果读取成功,输出文件内容
}
环境变量
os.Getenv(key string) string
:获取指定的环境变量的值。os.Setenv(key, value string) error
:设置指定的环境变量的值。
下面是设置和获取环境变量的示例代码:
package main
import (
"fmt"
"os"
)
func main() {
// 设置环境变量
os.Setenv("MY_ENV_VAR", "hello world")
fmt.Println("设置环境变量成功!")
// 获取环境变量
value := os.Getenv("MY_ENV_VAR")
fmt.Println("环境变量的值为:", value)
}
进程管理
os.Getpid() int
:获取当前进程的 ID。os.Getppid() int
:获取当前进程的父进程 ID。os.Exit(code int)
:终止当前进程并返回指定的退出码。
下面是获取当前进程 ID 和父进程 ID 的示例代码:
package main
import (
"fmt"
"os"
)
func main() {
pid := os.Getpid()
ppid := os.Getppid()
fmt.Printf("当前进程 ID:%d,父进程 ID:%d\n", pid, ppid)
}
json序列化
在 Go 语言中,可以使用 encoding/json
包来实现 JSON 序列化和反序列化。下面是一个示例代码,同时加上了详细的注释说明:
package main
import (
"encoding/json"
"fmt"
)
type Person struct { // 定义一个结构体类型
Name string `json:"name"` // 指定结构体字段对应的 JSON 名称
Age int `json:"age"`
}
func main() {
// JSON 序列化
p := Person{Name: "张三", Age: 18} // 创建一个 Person 类型的变量
b, err := json.Marshal(p) // 将 Person 类型的变量 p 转换为 JSON 字节切片
if err != nil { // 如果转换失败,输出错误信息并退出程序
fmt.Println("JSON 序列化失败:", err)
return
}
fmt.Println("JSON 序列化结果:", string(b)) // 如果转换成功,输出 JSON 字符串
// JSON 反序列化
var p1 Person // 定义一个 Person 类型的变量
err = json.Unmarshal(b, &p1) // 将 JSON 字节切片 b 转换为 Person 类型的变量 p1
if err != nil { // 如果转换失败,输出错误信息并退出程序
fmt.Println("JSON 反序列化失败:", err)
return
}
fmt.Printf("JSON 反序列化结果:name=%s, age=%d\n", p1.Name, p1.Age) // 如果转换成功,输出反序列化结果
}
在这个示例中,我们定义了一个 Person
结构体类型,其中包含 Name
和 Age
两个字段。我们使用 json
标签来指定结构体字段的 JSON 名称,这样在序列化和反序列化时就可以按照指定的名称进行转换。
接下来,我们使用 json.Marshal
函数将 Person
类型的变量 p
转换为 JSON 字节切片。如果转换失败,输出错误信息并退出程序。如果转换成功,输出 JSON 字符串。
然后,我们使用 json.Unmarshal
函数将 JSON 字节切片 b
转换为 Person
类型的变量 p1
。如果转换失败,输出错误信息并退出程序。如果转换成功,输出反序列化结果。
需要注意的是,JSON 序列化和反序列化时,结构体的字段必须是可导出的(即首字母大写),否则无法进行转换。
测试(testing包)
在 Go 语言中,可以使用 testing
包来进行单元测试和基准测试。下面是一个示例代码,同时加上了详细的注释说明:
package main
import (
"testing"
)
func add(a int, b int) int { // 定义一个简单的加法函数
return a + b
}
func TestAdd(t *testing.T) { // 定义一个单元测试函数
if add(1, 2) != 3 { // 如果 add(1, 2) 的结果不等于 3,测试失败
t.Error("测试失败!")
}
t.Log("测试通过!") // 如果 add(1, 2) 的结果等于 3,测试通过
}
func BenchmarkAdd(b *testing.B) { // 定义一个基准测试函数
for i := 0; i < b.N; i++ { // 执行 b.N 次测试
add(1, 2) // 调用 add(1, 2) 函数
}
}
我们定义了一个简单的加法函数 add
。然后,我们使用 testing
包来定义一个单元测试函数 TestAdd
和一个基准测试函数 BenchmarkAdd
。
在单元测试函数 TestAdd
中,我们使用 if
语句来判断 add(1, 2)
是否等于 3,如果不等于 3,测试失败,否则测试通过。我们使用 t.Error
函数来输出测试失败的信息,使用 t.Log
函数来输出测试通过的信息。
在基准测试函数 BenchmarkAdd
中,我们使用 b.N
来指定测试次数,然后使用 for
循环来执行 b.N
次测试。在测试过程中,我们调用 add(1, 2)
函数来进行计算。
依赖管理(Go Modules)
go mod init <module>
:初始化一个新的模块,生成go.mod
文件。go mod tidy
:根据go.mod
文件,检查当前模块所需的依赖,并将它们添加到go.mod
文件中。go mod vendor
:将当前模块所需的依赖复制到vendor/
目录中。go mod download
:下载当前模块所需的所有依赖。go mod graph
:打印当前模块依赖关系图。go mod verify
:验证依赖是否正确并且没有被篡改。go mod why <module>
:解释为什么需要依赖某个模块。go list -m all
:列出所有的依赖模块。
实践
(VMware Workstation,Ubuntu 22.04 LTS,FinalShell)
go version
mkdir Workspace
mkdir Workspace/Space{1..2}
mkdir Workspace/Space2/Space3
cd Workspace
go mod init Test_go
cat Test_go
(Space1,Test_main.go)
package main
import (
"fmt"
"Test_go/Space2"
math "Test_go/Space2/Space3"
"github.com/bytedance/sonic"
)
func main(){
fmt.Println(util.Name)
fmt.Println(util.Add(3,4))
fmt.Println(math.Add(1,2,3))
bytes,_ := sonic.Marshal("hello")
fmt.Println(string(bytes))
}
(Space2,a.go)
package util
import "fmt"
var Name="Mugetsu"
func Add(a,b int) int{
return a+b
}
func init(){
fmt.Println("init util package")
}
(Space3,b.go)
package maths
import "fmt"
func sub (a,b int) int{
return a-b
}
func init(){
fmt.Println("init maths package")
}
(Space3,c.go)
package maths
func Add(a,b,c int) int{
return a+sub(b,c)
}
go run Test_main.go
/*
init util package
init maths package
Mugetsu
7
0
"hello"
*/
import里用工作区相对路径,大写函数为可以跨路径调用,小写函数不能调用