一、前言
本篇笔记是观看 《Go 语言基础语法》 后记录的,下面是课程笔记。
二、基础语法
什么是 Go 语言?
Go 是由Google开发的开源编程语言,它的设计目标是简单、高效、可靠。Go 语言在处理并发性能、编译速度和代码可读性方面有很强的优势,适用于构建高性能、分布式、并发的应用程序。
Go 的特点
- 高性能、高并发:Go 语言通过
Goroutines
和Channels
的并发模型实现了轻量级的多线程编程,使得处理并发任务非常高效。- 语法简单、学习曲线平缓:Go 语言的设计目标之一是简洁易学,它摒弃了一些复杂的语法,使得代码更易读、易维护,并且学习门槛较低。
- 丰富的标准库:Go 语言提供了一个强大而丰富的标准库,涵盖了网络、文件处理、加密、并发等领域,这些标准库可以极大地简化开发任务。
- 完善的工具链:Go 语言提供了一套完善的工具链,包括代码格式化、构建工具、测试工具等,使得开发、测试、部署过程更加便捷。
- 静态链接:Go 语言的程序可以静态链接,这意味着可以将所有的依赖库打包到一个可执行文件中,简化了部署和分发过程。
- 快速编译:Go 语言的编译速度非常快,这对于开发效率是一个重要的优势。
- 跨平台:Go 语言支持跨平台编译,可以在不同的操作系统上编译运行,无需修改代码,提高了代码的可移植性。
- 垃圾回收:Go 语言拥有自动垃圾回收(
Garbage Collection
)机制,使得内存管理更加简单和安全,避免了常见的内存泄漏问题。这些特点使得 Go 语言在构建高性能、分布式、并发的应用程序时具有很大的优势,同时也适用于各种其他类型的软件开发。它的发展非常迅速,已经成为许多开发者喜爱的编程语言之一。
哪些公司在使用 Go 语言
Go 语言在近年来得到了广泛的应用和采用,许多知名的公司和项目都在使用 Go 语言来开发各种应用和服务。以下是一些主要使用 Go 语言的公司和应用场景的例子:
- 字节跳动:使用上万个微服务中的大部分都是用 Go 语言编写的。
- 腾讯、百度、美团、滴滴、深信服、平安、OPPO、知乎、去哪儿、360、金山、微博、哗呷哗哩、七牛等国内知名公司。
- PingCAP:负责开源数据库
TiDB
和TiKV
的公司。 - Google、Facebook:作为 Go 语言的创始公司,它们仍然在大量使用 Go 来支持各种项目和服务。
应用场景和领域:
- 云计算:Go 语言在云计算领域得到广泛应用,例如 Docker 和 Kubernetes 等云原生组件就是用 Go 实现的,这些组件负责容器化应用的管理和编排。
- 微服务:由于 Go 语言的高并发性能和轻量级的 Goroutines,它非常适合用于构建微服务架构,许多公司在微服务架构中使用 Go 来处理并发任务。
- 大数据:Go 语言在大数据领域也有应用,例如 Prometheus 是一个广泛使用的监控和报警系统,它也是用 Go 编写的。
- 区块链:由于 Go 语言的性能和并发特性,它在区块链领域也得到了应用,例如以太坊的客户端 Geth 就是用 Go 实现的。
字节跳动为什么全面拥抱 Go 语言
字节跳动全面拥抱 Go 语言有很多原因,以下是一些主要因素:
- 最初使用的 Python,由于性能问题换成了 Go:在字节跳动早期,一些服务可能最初是使用 Python 编写的。然而,Python 在某些情况下可能面临性能瓶颈,特别是在高并发和大规模请求处理方面。为了提高性能,字节跳动选择了将一部分关键服务迁移到 Go 语言。
- C++ 不太适合在线 Web 业务:C++ 是一门强大的编程语言,但对于在线 Web 业务而言,它可能相对复杂,开发效率较低。Go 语言的简单性和高效性使得它更适合用于处理字节跳动的在线服务和 Web 业务。
- 早期团队非 Java 背景:字节跳动创始团队并非 Java 背景,所以在选择编程语言时,并不特别局限于传统的企业级语言,而是选择了更加灵活和适合构建高性能服务的 Go 语言。
- 性能比较好:Go 语言因为其并发模型和优化的运行时,拥有出色的性能表现,尤其在高并发环境下表现优异,这也是字节跳动采用 Go 语言的一个重要原因。
- 部署简单、学习成本低:Go 语言的编译器可以将代码编译为独立的二进制文件,这使得部署和分发变得非常简单。此外,Go 语言的语法简洁,学习曲线平缓,开发者可以快速上手,降低了学习成本。
- 内部 RPC 和 HTTP 框架的推广:字节跳动内部推广了基于 Go 语言的 RPC(远程过程调用)和 HTTP 框架,这些框架可以极大地简化服务间的通信和调用,提高了开发效率和系统性能。
综合以上原因,Go 语言成为字节跳动内部非常受欢迎的编程语言,被广泛应用于公司内部的各种服务和业务,使得字节跳动能够更好地满足其业务需求并保持高效的开发和部署流程。
Hello World
下面是一个简单的 Go 语言程序,它会打印出 “hello world”:
package main
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
记录如下:
package main
: 这句代码代表这个文件属于main
包的一部分,main
包是程序的入口包。在 Go 语言中,所有的可执行程序必须包含一个名为main
的包。package main
表示当前文件属于一个名为main
的包,这是一个可执行程序的入口。import "fmt"
:import
语句用于导入需要使用的包。在这里,我们导入了fmt
包,它是 Go 语言的标准库中的一个包,提供了格式化输入输出的功能。func main() { ... }
: 这是程序的主函数。当我们运行一个 Go 语言程序时,程序会从main()
函数开始执行。fmt.Println("hello world")
: 这行代码调用了fmt
包中的Println
函数,用于将括号内的文本输出到控制台。在这里,它会输出"hello world"
。
这是一个简单的 Go 语言程序,用于展示 Go 的基本结构。
变量
下面是 Go 的一些基础知识:
- Go语言是一门强类型语言,每个变量都有它自己的变量类型。
- 常见的变量类型包括:
- 字符串:用于存储文本数据
- 整数:用于存储整数值,可以是正数、负数或零。
- 浮点型:用于存储带有小数点的数值。
- 布尔型:用于存储
true
或false
两个值,表示逻辑真或假。
- 字符串是Go语言的内置类型,可以直接通过加号拼接,也可以直接用等于号比较。
- 大部分运算符的使用和优先级和C或者C++类似,所以如果你熟悉C/C++,在Go语言中使用运算符会很熟悉。
- Go语言中变量的声明方式有两种:
- 使用
var
关键字:可以通过var name string = ""
这种方式声明变量。在这种方式中,通常可以根据初始化的值自动推导出变量的类型,如果有需要,也可以显式地写出变量的类型。 - 使用
:=
短变量声明:可以直接使用变量冒号等于值的方式声明变量,例如name := ""
。这种方式Go语言会自动推导变量的类型。
- 使用
- 常量的定义方式:
- 使用
const
关键字:可以将var
关键字替换为const
,例如const pi = 3.14
。常量的值在声明时必须指定,并且不能再更改。
- 使用
- Go语言中常量没有确定的类型:常量的类型会根据使用的上下文自动确定。这使得常量在一些场景中更加灵活和适应不同类型的需求。
下面是对一些代码的记录:
import (
"fmt"
"math"
)
这里导入了两个包:fmt
和 math
。fmt
包提供了格式化输入输出的功能,可以用来打印信息到控制台。math
包提供了数学函数和常数的支持。
var a = "initial"
在 Go 中,使用 var
关键字来声明变量。这里声明了一个变量 a
,并且赋值为字符串 "initial"
。
var b, c int = 1, 2
var d = true
- 第一行同时声明了两个整型变量
b
和c
,并分别初始化为 1 和 2。 - 第二行声明了一个布尔型变量
d
,并赋值为true
var e float64
声明了一个浮点型变量 e
,并且没有进行赋值操作。在 Go 中,未初始化的变量会被默认赋予其类型的零值,所以 e
现在的值是 0。
f := float32(e)
这是一种特殊的变量声明方式,称为短变量声明。声明了一个变量 f
,并将 e
的值转换为 float32
类型赋给 f
。
g := a + "foo"
使用短变量声明,声明了一个变量 g
,并将 a
的值与字符串 "foo"
进行拼接,得到 "initialfoo"
。
fmt.Println(a, b, c, d, e, f, g)
使用 fmt.Println()
函数打印输出多个值,用空格分隔。输出结果为 initial 1 2 true 0 0 initialfoo
const s string = "constant"
使用 const
关键字声明了一个常量 s
,并赋值为字符串 "constant"
。常量在声明时必须进行初始化,且一旦赋值后,其值不可再改变。
const h = 500000000
声明了一个常量 h
,并赋值为 500000000。
const i = 3e20 / h
声明了一个常量 i
,并将 3e20
(科学计数法表示的数,表示 3 乘以 10 的 20 次方)除以 h
的值进行计算并赋给 i
。
fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
使用 fmt.Println()
打印输出多个值,用空格分隔。输出结果为:
constant 500000000 6e+11 -0.28470407323754404 0.7591864109375384
这里对 h
和 i
进行了 math.Sin()
函数调用,分别计算了 h
和 i
的正弦值。请注意,这些输出可能在不同的系统上会有微小的差异,因为浮点数计算具有一定的精度限制。
if else
在 go 语言里面,if else 写法和 C/C++ 类似,不同点在于 go 语言中,if 后面没有括号。第二个不同点在于 if 后面必须接一个大括号,你无法像 C/C++ 那样,直接把 if 里面的语句和 if 写在同一行。
总结如下:
- 没有括号:在Go语言中,if条件后面没有括号,只需在条件表达式之后加上一个空格即可。这使得代码更加简洁。
- 必须使用大括号:在Go语言中,if条件后面必须紧接着使用大括号包裹条件满足时要执行的代码块。这种强制性的大括号限制确保了代码的可读性和一致性。
来看一个例子:
package main
import "fmt"
func main() {
num := 10
// 正确的 if-else 写法
if num > 5 {
fmt.Println("num 大于 5")
} else {
fmt.Println("num 不大于 5")
}
// 错误的写法(无法将条件语句与代码写在同一行)
// if num > 5 fmt.Println("num 大于 5")
// 错误的写法(缺少大括号)
// if num > 5
// fmt.Println("num 大于 5")
}
这样的写法使得代码结构更加清晰,并且避免了一些常见的错误。Go语言的设计哲学之一就是追求简洁而高效的代码书写方式。
再来看一个复合条件语句:
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
这个条件语句是一个复合条件语句,使用了初始化语句 num := 9
。这个初始化语句在条件判断前被执行,并且 num
的作用域仅限于这个if-else-if语句块。
在这个示例中,num
被初始化为9,然后依次进行以下条件判断:
- 第一个条件判断
num < 0
不成立,因为9不小于0,所以不执行对应的代码块。 - 第二个条件判断
num < 10
成立,因为9小于10,所以执行对应的代码块,输出”9 has 1 digit
“。 - 因为条件满足,所以不会执行else语句块。
循环
在Go语言中,循环只有一种形式,即for
循环。for
循环的灵活性使其可以用来实现各种不同类型的循环,包括while
循环和do-while
循环。
最简单的 for
循环就是使用无限循环(死循环)
for {
// 这是一个无限循环
// 可以在循环体内使用 break 跳出循环
// 或者使用 continue 来继续下一次循环
}
for
循环也可以用作类似于while
循环的结构,只需省略初始化语句和循环条件表达式:
格式为:
for condition {
// 循环体
// 当 condition 为 true 时,继续循环
// 当 condition 为 false 时,跳出循环
}
使用for
循环实现do-while
循环的方式如下:
for {
// 循环体
// 此处可以放置 do-while 循环的代码逻辑
if condition {
break
}
// 在此处放置 while 循环的条件表达式,当条件满足时跳出循环,否则继续执行循环体
}
当然,使用 Go 语言可以很容易地实现经典的 C 循环:
for (int i = 0; i < 10; i++) {
// 循环体
}
在Go中,可以通过类似的方式实现:
for i := 0; i < 10; i++ {
// 循环体
}
switch
当在Go语言中使用switch
分支结构时,有一些独特之处,让我们逐个记录:
switch
后的变量不需加括号:
在Go语言中,switch
后面的表达式无需括号包围,这与其他编程语言(如C/C++)中的switch
结构略有不同。例如:
num := 2
switch num {
case 1:
fmt.Println("One")
case 2:
fmt.Println("Two")
default:
fmt.Println("Other")
}
case
语句不需加break
:
在Go语言的switch
结构中,每个case
语句执行完毕后,不需要使用break
语句来跳出switch
块,这与C/C++中的switch
不同。Go语言中的switch
只会执行匹配的case
语句,执行完后自动跳出switch
块。
另外,如果需要执行多个case
语句(即多个条件满足时要执行相同的代码),可以使用逗号将多个条件合并在一起。例如:
num := 2
switch num {
case 1, 2, 3:
fmt.Println("One, Two, or Three")
default:
fmt.Println("Other")
}
switch
可以使用任意变量类型:
在Go语言中,switch
的表达式可以是任意类型的数据,不仅限于整数类型或字符类型,这使得switch
结构非常灵活。例如,可以使用字符串类型作为switch
表达式:
fruit := "apple"
switch fruit {
case "apple":
fmt.Println("It's an apple.")
case "orange":
fmt.Println("It's an orange.")
default:
fmt.Println("Unknown fruit.")
}
- 可以在
case
里面写条件分支:
在Go语言中,case
语句不仅仅限于常量匹配,还可以写更复杂的条件分支。例如:
num := 10
switch {
case num < 0:
fmt.Println("Negative number")
case num >= 0 && num < 10:
fmt.Println("Single-digit positive number")
case num >= 10 && num < 100:
fmt.Println("Double-digit positive number")
default:
fmt.Println("Large positive number")
}
在这个示例中,switch
后面没有表达式,而是直接在每个case
语句中写条件分支,根据num
的不同值,执行相应的代码块。
以上是Go语言中switch
分支结构的一些特点和用法。switch
的灵活性使得我们可以轻松地处理多种情况下的条件分支。
数组
在Go中,数组是一个具有编号且长度固定的元素序列,其中每个元素都具有相同的类型。数组的长度在创建时就确定,并且无法更改。
创建数组的基本语法为:
var arrayName [length]dataType
其中:
arrayName
是数组的名称length
是数组的长度(即包含的元素个数)dataType
是数组中元素的数据类型。
例如,创建一个长度为 5 的整数数组:
var intArray [5]int
数组元素的索引从0开始,可以通过索引来访问和修改数组的元素。例如,给数组的第一个元素赋值为10,可以使用以下语句:
intArray[0] = 10
要打印整个数组,可以使用fmt.Println
函数,它会将数组的所有元素打印出来:
fmt.Println(intArray)
此外,Go语言提供了更简洁的数组初始化方式,可以在创建数组时直接指定元素的值:
intArray := [5]int{10, 20, 30, 40, 50}
这样就创建了一个包含5个元素的整数数组,并给每个元素赋予了初始值。
下面创建一个2x3
的二维整数数组twoD
,并使用两个嵌套的for
循环对其进行初始化。接着,使用fmt.Println
函数打印整个二维数组。
2d: [[0 1 2] [1 2 3]]
切片
在真实业务代码中,更多使用的是切片。
Go语言中的数组是值类型,当将一个数组赋值给另一个数组时,会将整个数组内容复制一份。这可能导致在处理大型数组时出现性能问题。为了避免这种问题,可以使用切片(slice),切片是对数组的引用,它更灵活、更高效。
切片是对底层数组的一个视图,它包含了指向底层数组的指针、切片的长度和容量信息。由于切片是对数组的引用,因此修改切片中的元素会影响原始数组,反之亦然。切片支持自动扩容,当切片的容量不够时,Go会自动为其分配更大的底层数组,并将原有的数据复制到新的数组中。
创建切片有多种方式,其中最常见的是通过make
函数:
slice := make([]int, 5) // 创建一个初始长度为5的整数切片
通过make
函数,我们可以方便地创建指定类型和长度的切片,初始值都是该类型的零值。
另外,我们还可以通过切片字面量创建切片:
slice := []int{1, 2, 3, 4, 5} // 创建一个包含5个整数的切片
slice := []int{1, 2, 3, 4, 5}
:这是一个切片的创建方式。在初始化时没有指定长度,只有一个空的方括号[]
,表示创建一个切片。切片的长度是根据初始值的个数来确定的,所以这里的切片slice
长度是5,包含5个整数元素。intArray := [5]int{10, 20, 30, 40, 50}
:这是一个数组的创建方式。在初始化时,使用了固定长度的方括号[5]
,表示创建一个长度为5的整数数组。数组的长度在创建时就确定,无法更改。
切片可以通过索引来访问和修改元素,例如:
slice[0] = 10 // 修改第一个元素为10
fmt.Println(slice[3]) // 访问第四个元素的值
使用append
追加元素:
slice := []int{1, 2, 3}
slice = append(slice, 4)
通过append
函数,我们可以向切片中追加新的元素。如果切片的容量不足,会进行扩容,返回一个新的切片,因此需要将append
的结果赋值给原切片。
切片的切割操作:
slice := []int{1, 2, 3, 4, 5}
subSlice := slice[1:4] // 取出第二个到第四个位置的元素,得到[2, 3, 4]
切片支持Python风格的切片操作,可以方便地截取出子切片。
切片的优势:
- 动态长度:切片的长度可以根据需要动态增加或缩减,相比于数组的静态长度,切片在处理不确定长度的数据时更为方便。
- 函数传参:切片在函数参数传递时,不会像数组一样复制整个数据,而是复制指向底层数组的指针、长度和容量信息,这样可以避免大量数据的复制,节省内存和提高性能。
- 灵活的使用方式:切片支持切割、追加、复制和连接等操作,使得切片在各种场景下都有广泛的应用。
另外:
- 切片的长度和容量: 切片的长度表示当前包含的元素个数,而容量表示切片扩容前的底层数组可以容纳的元素个数。切片的容量是自动扩容的,具体扩容策略是在长度不断增加时,当容量不够时,Go会自动为其分配更大的底层数组,并将原有的数据复制到新的数组中。
- 切片的底层原理: 切片是对底层数组的一个引用,它包含了指向底层数组的指针、切片的长度和容量信息。因此,当多个切片引用同一个底层数组时,它们会共享相同的数据。
map
map,在其他编程语言中,可以叫做哈希或者字典。
map是实际使用过程中最常用的数据结构之一,当我们创建一个 map 时,需要有两个类型,第一个是 key 的类型,第二个是 value 的类型。我们可以从里面去存取键值对,也可以用 delete 来删除键值对。Go语言中的 map 是完全无序的,因此遍历的时候不会按照顺序来遍历,而是随机的。
下面是 map
的使用记录。
map
的创建和初始化:
在Go语言中,可以使用内置的make
函数来创建和初始化map
。
make
函数接受两个参数:map
的类型和初始容量(可选)。
例如:
// 创建一个键类型为string,值类型为int的空map
m := make(map[string]int)
// 创建一个键类型为string,值类型为string的map,并指定初始容量为10
m := make(map[string]string, 10)
- 存取键值对:
可以使用索引操作符[]
来存取map
中的键值对。例如:
m := make(map[string]int)
m["apple"] = 1 // 存储键值对
m["orange"] = 2
fmt.Println(m["apple"]) // 获取键值对中键为"apple"的值
fmt.Println(m["orange"]) // 获取键值对中键为"orange"的值
- 删除键值对:
使用内置的delete
函数可以删除map
中的键值对。例如:
m := make(map[string]int)
m["apple"] = 1
m["orange"] = 2
delete(m, "apple") // 删除键为"apple"的键值对
- 遍历
map
:
Go语言中,map
是无序的,因此遍历时无法保证顺序。可以使用range
关键字来遍历map
,获取其中的键值对。例如:
m := map[string]int{
"apple": 1,
"orange": 2,
}
// 使用 range 关键字来迭代 Map,从而在每次循环迭代中获取键和对应的值。
for key, value := range m {
// 每次迭代会获取 Map 中的一个键值对,此时打印键值对的值
fmt.Println(key, value)
}
range
在Go语言中,range
是一个关键字,用于在for
循环中迭代数组、切片、字符串、map和通道(channel)等数据结构。range
关键字提供了一种简洁的方式来遍历这些数据结构中的元素。
- 遍历数组和切片:
arr := [3]int{1, 2, 3}
for index, value := range arr {
// index是索引,value是对应索引处的值
fmt.Println(index, value)
}
在这个例子中,我们使用range
关键字遍历数组arr
,在每次迭代中,index
代表当前元素的索引,value
代表当前元素的值。输出将是:
0 1
1 2
2 3
- 遍历字符串:
str := "hello"
for index, char := range str {
// index是字符在字符串中的字节索引,char是对应字节处的Unicode字符
fmt.Println(index, char, string(char))
}
在这个例子中,我们使用range
关键字遍历字符串str
,在每次迭代中,index
代表当前字符的字节索引,char
代表当前字符的Unicode
值。输出将是:
0 104 h
1 101 e
2 108 l
3 108 l
4 111 o
- 遍历Map(映射):
m := map[string]int {
"one" : 1,
"two" : 2,
}
for key, value := range m {
// key是map中的键,value是对应键的值
fmt.Println(key, value)
}
在这个例子中,我们使用range
关键字遍历Mapm
,在每次迭代中,key
代表当前键,value
代表当前键对应的值。输出将是:
one 1
two 2
注意,当使用range
遍历字符串、数组、切片、map或通道时,会返回两个值:第一个是索引,第二个是对应位置的值。如果你不需要索引,可以使用下划线 _
来忽略它
比如:
str := "hello"
for _, char := range str {
// 使用下划线 _ 忽略索引,char是对应字节处的Unicode字符
fmt.Println(char)
}
函数
在 Go 语言中的函数,返回值的数据类型是后置的,Go 里面的函数原生支持返回多个值,在实际的业务逻辑代码中,几乎所有函数都返回两个值,第一个值是返回真正的结果,第一个值是一个错误信息。
下面是函数的知识点记录:
在 Go 语言中,函数的返回值声明是后置的。在函数签名中,可以指定多个返回值,格式如下:
func functionName(parameters) (returnType1, returnType2, ...) {
// 函数体
return value1, value2, ...
}
举个例子:
func add(a int, b int) int {
return a + b
}
这个函数add
接受两个int
类型的参数,并返回一个int
类型的结果。
另外,如果多个参数的类型相同,可以简化为在最后一个参数的类型前面写一次,其他参数省略类型。例如:
func add2(a, b int) int {
return a + b
}
Go语言中的函数可以返回多个值。例如,在你的exists
函数中:
func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok
}
这个函数exists
接受一个map[string]string
类型的参数m
和一个string
类型的参数k
,返回两个值:v
为map
中k
对应的值,ok
为布尔值,表示键是否存在。
在Go语言中,多返回值的一个常见用途是返回函数执行的结果以及错误信息。例如:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
这个函数divide
接受两个int
类型的参数a
和b
,返回两个值:第一个值为a/b
的结果,第二个值是一个error
类型,用于表示可能出现的错误情况。
指针
在Go语言中,指针是一种特殊的数据类型,用于存储变量的内存地址。通过指针,我们可以直接访问内存中的数据,这样可以对原始的变量进行修改,而不是对传入函数的副本进行操作。
来看这个函数:
func add2(n int) {
n += 2
}
这个函数接受一个int
类型的参数n
,在函数内部对参数n
进行操作。但是请注意,这里的n
是函数参数的一个副本,函数对其进行的任何修改都不会影响原始的变量。因此,在main
函数中调用add2(n)
后,n
的值仍然是5。
再来看下面这个函数:
func add2ptr(n *int) {
*n += 2
}
这个函数接受一个int
类型的指针n
,参数类型为*int
。在函数内部,通过*n
来获取指针指向的值,然后进行修改。因为参数是指针,函数对其进行的修改会影响原始的变量。
在main
函数中,我们通过add2ptr(&n)
将n
的地址传递给函数add2ptr
,所以函数会直接修改原始的变量。因此,n
的值会变为7。
小结
在Go语言中,通过指针可以直接访问内存中的数据,从而实现对原始变量的修改。使用指针时需要注意参数传递的方式,以及是否需要修改原始的变量。指针在函数中常用于修改函数外部的变量,以及在需要操作底层数据的情况下。
结构体
结构体(struct
)是Go
语言中一种自定义的数据类型,用于表示一组相关字段的集合。
结构体中的字段可以是不同的数据类型,可以是基本类型(如int
、string
等),也可以是其他自定义类型(如其他结构体或数组等)。
下面记录结构体的使用。
- 定义结构体:
type user struct {
name string
password string
}
这里定义了一个名为user
的结构体,它有两个字段:name
和password
,类型都是string
。
- 创建结构体变量
a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}
c.password = "1024"
var d user
d.name = "wang"
d.password = "1024"
这些语句创建了不同的user
类型的结构体变量,并为字段赋予了不同的初始值。a
和b
使用了不同的初始化方式,而c
和d
则先创建结构体变量,然后再逐个赋值。
- 结构体作为函数参数:
func checkPassword(u user, password string) bool {
return u.password == password
}
func checkPassword2(u *user, password string) bool {
return u.password == password
}
两个函数分别接受user
类型和*user
类型的结构体作为参数:
checkPassword
函数传递结构体值作为参数checkPassword2
函数传递结构体指针作为参数。- 因为结构体作为参数传递时会进行拷贝,所以在
checkPassword
函数中对结构体的修改不会影响原始结构体。 - 而在
checkPassword2
函数中传递指针,可以对原始结构体进行修改。
小结
结构体是
Go
语言中一种自定义的数据类型,用于表示一组相关字段的集合。它可以包含不同类型的字段,并且可以进行初始化和修改。结构体作为函数参数时,传递结构体值会进行拷贝,而传递结构体指针则可以对原始结构体进行修改。结构体在Go语言中是非常常用的数据结构,常用于组织和管理数据。
错误处理
在Go语言中,错误处理是一种常见的编程习惯,并且Go语言提供了一种简洁且明确的错误处理机制。
不同于 Java 使用的异常。
go
语言的处理方式,能够很清晰地知道哪个函数返回了错误,并且能用简单的if else
来处理错误
来看下面这个 findUser
函数:
import (
"errors"
"fmt"
)
type user struct {
name string
password string
}
func findUser(users []user, name string) (v *user, err error) {
for _, u := range users {
if u.name == name {
return &u, nil
}
}
return nil, errors.New("not found")
}
这个函数findUser
接受一个users
切片和name
字符串作为参数,并返回一个*user
类型的指针和一个error
类型的错误。函数首先遍历users
切片,查找是否有与name
匹配的用户。如果找到了,就返回指向该用户的指针,同时error
为nil
表示没有错误。如果没有找到,就返回nil
指针和一个自定义的not found
错误。
错误处理示例:
u, err := findUser([]user{{"wang", "1024"}}, "wang")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name) // wang
if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
fmt.Println(err) // not found
return
} else {
fmt.Println(u.name)
}
这里展示了两种错误处理的情况:
- 第一种情况,调用
findUser
函数查找name
为"wang"
的用户。因为存在该用户,所以函数返回了指向该用户的指针,并且err
为nil
表示没有错误。所以输出结果为wang
。 - 第二种情况,调用
findUser
函数查找name
为"li"
的用户。由于不存在该用户,所以函数返回了nil
指针和一个自定义的错误not found
。因此,在if err != nil
条件下,我们输出了错误信息not found
。
小结
Go
语言中的错误处理是一种常见的习惯,通过在函数的返回值中使用error
类型,可以清晰地知道哪个函数可能会返回错误。当函数遇到错误时,通常会返回nil
值和一个错误对象,而当没有错误时,返回实际结果和nil
错误。使用简单的if else
结构就可以处理函数返回的错误,这样可以使代码更加简洁和易读。错误处理是Go
语言编程中非常重要的部分,合理处理错误有助于提高代码的健壮性和可靠性。
字符串操作
Go语言的标准库strings
包提供了对字符串进行处理的一系列函数。这些函数包括字符串搜索、切割、连接、替换、大小写转换等操作,使得字符串处理变得简单、高效和易于使用。
记录如下:
func Contains(s, substr string) bool
:- 功能:判断字符串
s
中是否包含子字符串substr
。 - 返回值:如果包含,则返回
true
,否则返回false
。
- 功能:判断字符串
func Count(s, substr string) int
:- 功能:统计字符串
s
中子字符串substr
的出现次数。 - 返回值:返回子字符串出现的次数。
- 功能:统计字符串
func HasPrefix(s, prefix string) bool
:- 功能:判断字符串
s
是否以子字符串prefix
开头。 - 返回值:如果以
prefix
开头,则返回true
,否则返回false
。
- 功能:判断字符串
func HasSuffix(s, suffix string) bool
:- 功能:判断字符串
s
是否以子字符串suffix
结尾。 - 返回值:如果以
suffix
结尾,则返回true
,否则返回false
。
- 功能:判断字符串
func Index(s, substr string) int
:- 功能:查找字符串
s
中子字符串substr
的第一个出现位置。 - 返回值:如果找到,则返回子字符串的索引值;如果未找到,则返回
-1
。
- 功能:查找字符串
func Join(elems []string, sep string) string
:- 功能:将字符串切片
elems
使用sep
连接成一个字符串。 - 返回值:返回连接后的字符串。
- 功能:将字符串切片
func Repeat(s string, count int) string
:- 功能:将字符串
s
重复count
次。 - 返回值:返回重复后的字符串。
- 功能:将字符串
func Replace(s, old, new string, n int) string
:- 功能:将字符串
s
中前n
个子字符串old
替换为new
。 - 返回值:返回替换后的字符串。
- 注意:如果
n
大于0:表示最多替换n
个匹配的子字符串。如果n
小于0:表示替换所有匹配的子字符串。如果n
等于0:表示不替换任何匹配的子字符串。
- 功能:将字符串
func Split(s, sep string) []string
:- 功能:将字符串
s
按照分隔符sep
进行切割,返回切割后的字符串切片。 - 返回值:返回切割后的字符串切片。
- 功能:将字符串
func ToLower(s string) string
:- 功能:将字符串
s
中的所有字符转换为小写形式。 - 返回值:返回转换后的字符串。
- 功能:将字符串
func ToUpper(s string) string
:- 功能:将字符串
s
中的所有字符转换为大写形式。 - 返回值:返回转换后的字符串。
- 功能:将字符串
来看下面的例子:
package main
import (
"fmt"
"strings"
)
func main() {
// 1. Contains函数示例
a := "hello"
fmt.Println(strings.Contains(a, "ll")) // true
// 2. Count函数示例
b := "hello, hello"
fmt.Println(strings.Count(b, "hello")) // 2
// 3. HasPrefix函数示例
c := "hello"
fmt.Println(strings.HasPrefix(c, "he")) // true
// 4. HasSuffix函数示例
d := "hello"
fmt.Println(strings.HasSuffix(d, "llo")) // true
// 5. Index函数示例
e := "hello"
fmt.Println(strings.Index(e, "ll")) // 2
// 6. Join函数示例
f := []string{"he", "llo"}
fmt.Println(strings.Join(f, "-")) // he-llo
// 7. Repeat函数示例
g := "hello"
fmt.Println(strings.Repeat(g, 2)) // hellohello
// 8. Replace函数示例
h := "hello"
fmt.Println(strings.Replace(h, "e", "E", -1)) // hEllo
// 9. Split函数示例
i := "a-b-c"
fmt.Println(strings.Split(i, "-")) // [a b c]
// 10. ToLower函数示例
j := "Hello"
fmt.Println(strings.ToLower(j)) // hello
// 11. ToUpper函数示例
k := "hello"
fmt.Println(strings.ToUpper(k)) // HELLO
}
字符串格式化
Go语言的标准库fmt
包提供了丰富的字符串格式化方法,使得打印和输出数据更加方便和灵活。比如 printf
这个类似于 C
语言里面的 printf
函数。不同的是,在go语言里面的话,你可以很轻松地用 %v
来打印任意类型的变量,而不需要区分数字字符串。你也可以用 %+v
打印详细结果,%#v
则更详细。
记录如下:
%v
:默认格式化动词,用于打印任意类型的变量。%+v
:包含字段名的格式化动词,用于打印结构体时,会将字段名也打印出来。%#v
:完整Go语法表示的格式化动词,用于打印结构体时,会打印出结构体的完整Go语法表示,包括包名。
在Go语言中,使用
fmt
提供的字符串格式化函数非常方便,不需要像C语言中那样去区分不同的格式化函数,而是通过简单的格式化动词来处理不同类型的数据。这样使得字符串格式化更加简洁、易于理解和使用。在实际的开发中,字符串格式化是一个非常常用且有用的功能,用于输出调试信息、日志记录以及展示结果等场景。
来看下面这段示例代码:
package main
import "fmt"
// 定义一个名为point的结构体,包含两个整型字段x和y
type point struct {
x, y int
}
func main() {
// 定义一个字符串变量s,并赋值为"hello"
s := "hello"
// 定义一个整数变量n,并赋值为123
n := 123
// 定义一个名为p的结构体变量,并赋值为point{1, 2}
p := point{1, 2}
// 使用fmt.Println函数打印s和n的值,结果为"hello 123"
fmt.Println(s, n)
// 使用fmt.Println函数打印p的值,结果为"{1 2}"
fmt.Println(p)
// 使用fmt.Printf函数打印格式化后的字符串,%v为默认格式化动词
// 输出结果为:s=hello
fmt.Printf("s=%v\n", s)
// 使用fmt.Printf函数打印格式化后的字符串,%v为默认格式化动词
// 输出结果为:n=123
fmt.Printf("n=%v\n", n)
// 使用fmt.Printf函数打印格式化后的字符串,%v为默认格式化动词
// 输出结果为:p={1 2}
fmt.Printf("p=%v\n", p)
// 使用fmt.Printf函数打印格式化后的字符串,%+v为包含字段名的格式化动词
// 输出结果为:p={x:1 y:2}
fmt.Printf("p=%+v\n", p)
// 使用fmt.Printf函数打印格式化后的字符串,%#v为完整Go语法表示的格式化动词
// 输出结果为:p=main.point{x:1, y:2}
fmt.Printf("p=%#v\n", p)
// 定义一个浮点数变量f,并赋值为3.141592653
f := 3.141592653
// 使用fmt.Println函数打印f的值,结果为3.141592653
fmt.Println(f)
// 使用fmt.Printf函数打印格式化后的浮点数,%.2f为保留两位小数的格式化动词
// 输出结果为:3.14
fmt.Printf("%.2f\n", f)
}
JSON 处理
Go语言中的JSON操作非常简便,标准库encoding/json
提供了Marshal
和Unmarshal
函数用于JSON的序列化和反序列化。
来看下面这个例子:
package main
import (
"encoding/json"
"fmt"
)
// 定义一个userInfo结构体,用于演示JSON的序列化和反序列化
type userInfo struct {
Name string // 字段名首字母大写,可被JSON序列化
Age int `json:"age"` // 使用JSON tag指定输出JSON结果里的字段名为"age"
Hobby []string // 字段名首字母大写,可被JSON序列化
}
func main() {
// 创建一个userInfo结构体变量a,并赋值
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
// 使用json.Marshal函数对userInfo结构体进行序列化
buf, err := json.Marshal(a)
if err != nil {
panic(err)
}
fmt.Println(buf) // 序列化后的JSON字符串对应的字节切片
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
// 使用json.MarshalIndent函数对userInfo结构体进行格式化序列化
buf, err = json.MarshalIndent(a, "", "\t")
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // 格式化后的JSON字符串
// 创建一个空的userInfo结构体变量b
var b userInfo
// 使用json.Unmarshal函数将JSON字符串反序列化到userInfo结构体变量b中
err = json.Unmarshal(buf, &b)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", b) // 反序列化后的userInfo结构体变量b
}
运行结果:
[123 34 78 97 109 101 34 58 34 119 97 110 103 34 44 34 97 103 101 34 58 49 56 44 34 72 111 98 98 121 34 58 91 34 71 111 108 97 110 103 34 44 34 84 121 112 101 83 99 114 105 112 116 34 93 125]
{"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
{
"Name": "wang",
"age": 18,
"Hobby": [
"Golang",
"TypeScript"
]
}
main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
通过这些操作,Go语言中的JSON处理
变得非常简单和灵活,非常适合用于数据交换和存储。
另外,JSON tag是Go语言结构体字段后面的特殊注释,用于修改默认的JSON序列化和反序列化行为。在Go语言中,结构体的字段可以添加json:"tag"
来指定JSON字段名。通过使用JSON tag,我们可以控制在序列化和反序列化时,结构体字段与JSON字段之间的映射关系。
JSON tag的格式是json:"tag"
,其中tag
是要指定的JSON字段名。如果结构体字段的tag为空,那么在进行JSON序列化时会使用字段的名称作为JSON字段名。而当tag不为空时,会使用tag指定的字段名作为JSON字段名。
举个例子:
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"username"`
Age int `json:"age"`
Email string `json:"-"` // "-" 表示忽略该字段,不参与JSON序列化和反序列化
}
func main() {
u := User{
Name: "John",
Age: 30,
Email: "john@example.com",
}
// 使用json.MarshalIndent函数对User结构体进行格式化序列化
buf, err := json.MarshalIndent(u, "", "\t")
if err != nil {
panic(err)
}
fmt.Println(string(buf))
}
在上面的例子中,我们定义了一个User
结构体,其中Name
字段使用了JSON tagjson:"username"
,Age
字段使用了JSON tagjson:"age"
,Email
字段使用了JSON tagjson:"-"
,表示忽略该字段。当我们将User
结构体进行JSON序列化时,输出结果如下:
{
"username": "John",
"age": 30
}
通过使用JSON tag,我们可以灵活地控制JSON序列化和反序列化的行为,使得结构体字段和JSON字段之间的映射关系更加自定义和灵活。
时间处理
在Go语言中,时间处理使用time
包提供的函数和方法非常简单方便。我们可以获取当前时间、构造指定时区的时间、格式化时间输出、计算时间差等。同时,Go语言的时间处理也支持时间戳和字符串之间的转换,使得时间在不同格式之间的转换更加灵活和便捷。
来看下面这个代码:
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now() // 获取当前时间
fmt.Println(now) // 2023-07-26 17:01:24.4630365 +0800 CST m=+0.002492201
t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC) // 创建一个指定日期和时间的时间对象t,并且使用time.UTC时区
t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
fmt.Println(t) // 2022-03-27 01:25:36 +0000 UTC
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2022-03-27 01:25:36
diff := t2.Sub(t)
fmt.Println(diff) // 1h5m0s
fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
if err != nil {
panic(err)
}
fmt.Println(t3 == t) // true
fmt.Println(now.Unix()) // 1690362084
}
记录如下:
- 在Go语言中,通过
time.Now()
函数可以获取当前的本地时间,返回一个time.Time
类型的值。时间对象time.Time
包含了年、月、日、小时、分钟、秒等信息。 - 使用
time.Date()
函数可以构造指定时区的时间对象。其中,time.Date()
函数接收年、月、日、小时、分钟、秒、纳秒和时区等参数,并返回一个对应的time.Time
类型的值。 - 使用
time.Format()
方法可以将时间对象格式化为指定的字符串形式。在格式化字符串中,特定的数字表示特定的时间部分(如年份用2006表示,月份用01表示,小时用15表示,分钟用04表示,秒用05表示)。这是Go语言时间格式化的特殊规定。 - 使用
time.Sub()
方法可以计算两个时间点之间的时间差,返回一个time.Duration
类型的值。 - 使用
time.Unix()
方法可以将时间对象转换为Unix时间戳,表示从1970年1月1日到该时间的秒数。 - 使用
time.Parse()
函数可以将字符串解析成对应的时间对象。在解析时,需要提供一个匹配时间格式的字符串作为解析规则。
数字解析
在 Go 语言中,strconv
(string convert)是一个标准库的包,用于在字符串和基本数据类型之间进行转换。它提供了一系列函数,用于将字符串解析成基本数据类型,或将基本数据类型转换为字符串。
来看下面这段代码:
package main
import (
"fmt"
"strconv"
)
func main() {
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111
n, _ = strconv.ParseInt("0x1000", 0, 64)
fmt.Println(n) // 4096
n2, _ := strconv.Atoi("123")
fmt.Println(n2) // 123
n2, err := strconv.Atoi("AAA")
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}
下面对上述代码进行记录:
f, _ := strconv.ParseFloat("1.234", 64)
将字符串"1.234"
解析为64位浮点数,并将结果赋值给变量f
。这里使用了strconv.ParseFloat
函数,第二个参数64
表示要解析成64位浮点数。输出结果为1.234
。
n, _ := strconv.ParseInt("111", 10, 64)
将字符串"111"
解析为64位有符号整数,并将结果赋值给变量n
。这里使用了strconv.ParseInt
函数,第二个参数10
表示使用十进制进行解析,第三个参数64
表示要解析成64位整数。输出结果为111
。
n, _ = strconv.ParseInt("0x1000", 0, 64)
将字符串"0x1000"
解析为64位整数,并将结果赋值给变量n
。这里使用了strconv.ParseInt
函数,第二个参数0
表示自动判断字符串的进制,第三个参数64
表示要解析成64位整数。输出结果为4096
,因为0x1000
是十六进制表示的数,转换为十进制就是4096
。
n2, _ := strconv.Atoi("123")
将字符串"123"
转换为整数,并将结果赋值给变量n2
。这里使用了strconv.Atoi
函数,它是strconv.ParseInt
的一种简化版本,用于转换常见的整数表示。输出结果为123
。
n2, err := strconv.Atoi("AAA")
将字符串"AAA"
转换为整数,并将结果赋值给变量n2
。由于输入不合法(非数字字符串),转换失败,同时返回一个错误信息err
。输出结果为0
,表示转换失败,同时打印错误信息strconv.Atoi: parsing "AAA": invalid syntax
。
进程信息
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// go run example/20-env/main.go a b c d
fmt.Println(os.Args) // 输出程序启动时的命令行参数。os.Args是一个字符串切片,包含了程序名称和所有的命令行参数。
fmt.Println(os.Getenv("PATH")) // 输出环境变量PATH的值。os.Getenv用于获取指定环境变量的值。
fmt.Println(os.Setenv("AA", "BB")) // 设置环境变量AA的值为BB。os.Setenv用于设置指定环境变量的值。
buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
// 创建一个用于执行grep命令的*exec.Cmd结构体。
// 命令名称是grep,参数是"127.0.0.1"和"/etc/hosts"。
// 执行命令并获取其输出结果。CombinedOutput是*exec.Cmd结构体的方法,它返回命令的输出结果和执行过程中的错误(如果有的话)。
if err != nil {
panic(err) // 检查命令执行是否发生错误,如果有错误,则使用panic函数中断程序执行并输出错误信息。
}
fmt.Println(string(buf)) // 将命令执行的结果(存储在buf中)转换为字符串并输出。
// 在这个例子中,grep命令在/etc/hosts文件中查找包含127.0.0.1的行,并将结果输出。
}
三、遇到的问题 – VScode 安装 tools 失败
在刚开始使用 VScode 编码 Go 文件时, VScode 会提示安装插件,此时插件安装成功后,我们需要更新 Go 工具,此时可能因为网络原因而安装失败。
最后通过修改修改 GOPROXY
的值后,问题解决:
go env -w GOPROXY=https://goproxy.cn,direct
使用
go env
命令可以看到 Go 的环境配置。