Go 语言速成指南(二)
把所有的代码跟着敲一遍,基本语法全部能掌握🫡
切片
Go语言中的切片是对数组的抽象,它提供了一个更为强大且灵活的接口来操作数组的子序列。切片是可变的,可以动态地增加和减少大小。
切片声明
// 创建一个空切片,长度和容量都为0
var s []int
fmt.Println("Empty slice: ", s)
// 用make创建一个指定类型、长度和容量的切片
s = make([]int, 3) // 创建一个长度为3的切片, 默认element是0
fmt.Println("make创建的切片: ", s)
切片初始化
// 切片初始化
s1 := []int{1, 2, 3, 4}
fmt.Println("init slice: ", s1)
// 切片的零值是 nil
var nilSlice []int
if nilSlice == nil {
fmt.Println("nilSlice is nil")
}
切片的基础用法
// 遍历切片和遍历数组的方式一致,也是建议使用 for range方式遍历
// 切片长度和容量
sl := []int{1, 1, 1, 1, 1, 1, 1, 1}
// 使用 len() 函数获取长度
fmt.Println("Length of s ", len(sl))
// 使用cap() 函数获取容量
fmt.Println("Capacity of s ", cap(sl))
// 向切片中添加元素
s = append(s, 3, 4, 5)
fmt.Println("append element: ", s)
// 切片的复制操作
copyS := make([]int, len(sl))
copy(copyS, sl)
fmt.Println("Copy sl to copyS ", copyS)
// 创建一个动态数组,实现动态添加元素
dynamicSlice := []int{}
for i := 0; i < 10; i++ {
dynamicSlice = append(dynamicSlice, i)
}
fmt.Println("dynamicSlice is ", dynamicSlice)
注:切片的长度和切片的容量不能混为一谈。一个切片里有多少个元素,就是多大的长度,但容量可以认为是系统给分配的额度。就好比一个电梯标定能承载13个人,但这趟电梯里有3个人,此时13是容量,3是长度。
切片的特性
// 改变切片的元素会影响原始数组
myArray := [5]int{1, 2, 3, 4, 5}
mySlice := myArray[:4]
mySlice[0] = 100
fmt.Println("myArray: ", myArray)
fmt.Println("mySlice: ", mySlice)
myArray: [100 2 3 4 5]
mySlice: [100 2 3 4]
注:上述示例也能看出,切片中元素的改变会影响到原数组的改变。因此,可以通过传递切片的方式来对原数组中的元素进行修改。
map
在Go语言中,map是一种内置的key-value形式的数据结构,其内在还是一个哈希表的映射,因此存放的{key, value}一般是无序存放的。map里的Key要求是唯一的,值可以是任何类型。其中Key必须支持==和!=的比较运算符,因此,Key不能是切片、函数、map类型以及包含以上三种类型的任意类型。
创建map
// 创建map
m := make(map[string]string)
if m == nil {
fmt.Println("emptyMap is nil")
}
fmt.Println("m: ", m)
// 创建一个空map
var emptyMap map[string]int
//emptyMap["k1"] = 2 // 不能向空Map里添加键值对,因为系统没有给空map分配空间
//emptyMap = make(map[string]int) // 分配空间后,才可以使用
//emptyMap["k1"] = 2
if emptyMap == nil {
fmt.Println("emptyMap is nil")
}
注:使用make创建map,是指在系统里分配一块内存,m指向这片内存,因此m != nil。而直接创建的map是没有系统分配内存的,属于空map。因此,emptyMap == nil。要注意,再没有为emptyMap分配空间时,不可以向emptyMap里添加键值对,否则会引发panic错误。正确做法是先使用make函数为emptyMap分配空间,然后再向emptyMap里添加键值对。
map的常见操作
// 设置键值对
m["k1"] = "first"
m["k2"] = "second"
fmt.Println("m: ", m)
// 获取键k1值
v := m["k1"]
fmt.Println("value: ", v)
// 使用len() 返回 map中的键值对数量
fmt.Println("len: ", len(m))
// 内置的delete移除键值对
delete(m, "k1")
// 当获取的map中不存在的键值对,有默认的返回值
v = m["k3"]
fmt.Println("v: ", v)
// 可以通过两个变量接收的方式,判断键是否存在
_, ok := m["k2"]
if ok {
fmt.Println("key k2 exist? ", ok)
}
// 遍历是for range
for key, value := range m {
fmt.Printf("key: %s, value: %s\n", key, value)
}
指针
传值和传引用
package main
import "fmt"
func incrementValue(val int) {
val++
}
func incrementPoint(val *int) {
*val++
}
func main() {
a := 10
fmt.Println("original 'a' value: ", a)
// 传值
incrementValue(a)
fmt.Println("'a' after incrementValue: ", a)
// 传引用
incrementPoint(&a)
fmt.Println("'a' after incrementPoint: ", a)
}
original 'a' value: 10
'a' after incrementValue: 10
'a' after incrementPoint: 11
在Go语言中,函数参数默认是通过“值传递”的,即调用函数时会将实际参数值的副本传入函数。因此,如果你想在函数内部修改原始变量的值,那就必须用到引用传递。引用传递是指在传递参数时,传递原始变量的地址,然后函数拿到原始变量的地址,通过“*”的方式解引用,就可以从内存地址获取到当前值,此时,这个当前值就是原始变量的值,你针对当前值的修改会影响到原始变量的值。
基本类型的引用传递的使用方式和上述示例差不多,这里就不多写示例了,下面主要介绍一些数据结构类型的引用传递。
引用传递修改切片
在切片小节中讲过这么一句话“切片中元素的改变会影响到原数组的改变。因此,可以通过传递切片的方式来对原数组中的元素进行修改”。但是当我们把切片传递到函数中,新增了一个元素后,切片没有发生任何变化,这是为什么?这个稍微有点复杂,但是且听我慢慢道来。
首先,传递切片,在函数中修改切片,确实能做到对原始切片中的元素修改,这一点毋庸置疑。但是,下面的示例中,我做的不是修改元素操作,而是增加元素的操作。切片是有自己的容量大小的,当容量不足以放下这个元素时,系统会重新分配一个内存空间,原先的数据都会被拷贝到新的内存空间,然后返回一个指向该内存的指针。因此,append操作后,slice指向的地址已经不是原来切片的地址了,所以打印s并不会有预期的结果。
相反,当我们传递切片指针,这个问题就迎刃而解了。因为当append操作后,*slice指针会指向新的地址,这就意味着它改变了原始切片的地址,让原始切片的地址指向了一个新地址,这样再打印s,就是新地址里存储的元素。
func appendSlice(slice []int, value int) {
slice = append(slice, value)
}
func appendSlicePoint(slice *[]int, value int) {
*slice = append(*slice, value)
}
func main() {
// 引用传递 slice
s := []int{1, 2, 3}
appendSlice(s, 4)
fmt.Println("s: ", s)
appendSlicePoint(&s, 4)
fmt.Println("s: ", s)
}
s: [1 2 3]
s: [1 2 3 4]
引用传递修改map
map是引用类型,传入函数是map的引用,函数内对子弟所做的操作也会影响到原字典。
func addEmpty(m map[string]int, key string, value int) {
m[key] = value
}
func main() {
// 引用传递Map
m := make(map[string]int)
addEmpty(m, "k1", 1)
fmt.Println("m: ", m)
}
引用传递修改struct
结构体的用法其实和C语言一致。但是稍微有点不同的是,Go语言里的取结构体元素时,只能通过“.”获取,没有”->”这种方式获取。整体来说其实还是比较人性化的,没有那么多弯弯绕绕。想要改变结构体内的元素,那就使用下面的引用传递方式。
type Person struct {
Name string
Age int
}
func birthday(p *Person) {
p.Age++
}
func main() {
// 引用传递结构体
jiamu := Person{Name: "jiamu", Age: 18}
birthday(&jiamu)
fmt.Println(jiamu)
}
字符串
在Go语言中,字符串是不可变的字节序列,它可以包含任意的数据,包括字节的0值。每个字符串都可以看作是由byte类型的数组组成。字符串可以包含UTF-8编码的文本,因此可以表示多种语言的字符。Go语言中的rune类型表示单个Unicode字符。
字符串编码
func main() {
var s1 string = "hello world"
s2 := "你好,世界"
// 查看长度
fmt.Println("The length of s1: ", len(s1))
fmt.Println("The length of s2: ", len(s2))
// 单字符打印
for i := 0; i < len(s2); i++ {
fmt.Printf("%x ", s2[i])
}
fmt.Println()
for _, r := range s2 {
fmt.Printf("%x ", r)
}
fmt.Println()
}
The length of s1: 11
The length of s2: 15
e4 bd a0 e5 a5 bd ef bc 8c e4 b8 96 e7 95 8c
4f60 597d ff0c 4e16 754c
在存储时,字符占用的位数取决于所使用的编码方式,Go默认使用UTF-8编码方式。当存储Ascii字符时,一般是一个字符对应一个字节存储。因此s1的长度是11。但是,对于unicode字符则不然。如果使用len()函数计算,5个中文字符的长度,却显示是15。这是因为在UTF-8编码中,中文需要用3个字节表示,所以按照字节长度计算就是3*5=15。
打印中文字符时,使用for range方法,可以做到打印每一个字符,不要使用单字节打印,这样会导致字符串被不正确的切分。
字符串切分
// 中文字符串截取
r := []rune(s2)
// 单字符打印
for i := 0; i < len(r); i++ {
fmt.Printf("char %d: %c\n", i, r[i])
}
// 截取中文字符串的前两个文字
substr := string(r[:2])
fmt.Println("substr: ", substr)
char 0: 你
char 1: 好
char 2: ,
char 3: 世
char 4: 界
substr: 你好
字符串基本操作
// 字符串拼接
s3 := s1 + s2
fmt.Println(s3)
// 字符串比较
if s1 == "hello world" {
fmt.Println("s1 equals to 'hello world'")
}
// 字符串分割
parts := strings.Split(s3, " ")
for _, part := range parts {
fmt.Println(part)
}
// 字符串包含
if strings.Contains(s3, "世界") {
fmt.Println("s3 contains '世界'")
}
// 字符串替换,替换所有匹配项(n = -1)
news3 := strings.Replace(s3, "hello", "hi", -1)
fmt.Println(news3)
// 子字符串判断
isPrefix := strings.HasPrefix(s3, "hello")
isSuffix := strings.HasSuffix(s3, "世界")
fmt.Println("Prefix: ", isPrefix, "Suffix: ", isSuffix)
函数
在Go语言中,函数是基本的代码块,可以被多次调用和复用。Go支持匿名函数,它们可用于定义内联函数以及实现函数式编程。
函数定义和调用
func add(a int, b int) int {
return a + b
}
func main() {
// 调用add函数
result := add(2, 3)
fmt.Println("result: ", result)
}
上述示例定义了一个简单的add函数,并在main函数里调用了add函数。不难看出,Go语言的函数定义和调用和其它语言类似,只要掌握上述简单的函数定义方法和调用方法,就能做到触类旁通,写更多的自定义函数。
匿名函数
匿名函数是没有函数名的函数。在Go里,可以直接定义一个匿名函数并立即执行,也可以将匿名函数赋值给一个变量稍后执行。
// 匿名函数直接执行
func(message string) {
fmt.Println(message)
}("hello world")
// 匿名函数赋值给变量
sum := func(a int, b int) int {
return a + b
}
fmt.Println("sum : ", sum(3, 5))
闭包
func closure() func() int {
sum := 0
return func() int {
sum++
return sum
}
}
func main() {
increment := closure()
fmt.Println(increment())
fmt.Println(increment())
fmt.Println(increment())
}
1
2
3
闭包最好的理解就是要和“生成器”这个概念挂钩。就好比当我们触发一次生成器,原来的数值就会+1。从形状上来看,闭包像是函数的返回值是一个函数,函数体里有一个函数。现在我们将函数体里的函数抽象成一个变量,这个变量和闭包函数的变量共享一个作用域,因此,可以互相“可见”。
函数式编程
在Go中,可以将函数作为参数传递给其它函数,也可以将函数作为其它函数的返回值。
func createAdder() func(int) int {
return func(addend int) int {
return addend + 5
}
}
func main() {
// 函数作为参数
sum1 := applyFunc(10, 20, add)
fmt.Println("10 + 20 = ", sum1)
// 函数作为返回值
adder := createAdder()
fmt.Println("40 + 5 = ", adder(40))
}
面向对象
Go语言并不是面向对象编程语言,没有类和继承的概念,但是他支持一些面向对象的重要特性。比如用结构体表示对象的属性,使用接口提供多态。
结构体
Go语言的结构体和C语言很相似。
- 结构体定义
type Person struct {
Name string
Age int
Height float32
}
- 结构体初始化
p1 := Person{Name: "Alice", Age: 20, Height: 1.65} // 定义一个Person类型的结构体变量p1并初始化
p2 := new(Person) // 定义一个指向Person类型的指针变量p2,并分配内存空间
- 结构体元素访问(”.”号访问)
指针和普通的对象类型都是使用“.”号访问。
p1.Name = "Alice" // 给p1的Name赋值为"Alice"
p1.Age = 20 // 给p1的Age赋值为20
p1.Height = 1.65 // 给p1的Height赋值为1.65
面向对象特性
Go还支持一些面向对象的编程特性,非常的灵活和强大!!!
func (p *Person) GetInfo() string {
return fmt.Sprintf("Name: %s, Age: %d, Height: %.2f", p.Name, p.Age, p.Height)
}
p1.GetInfo() // 调用p1的GetInfo方法,返回"Name: Alice, Age: 20, Height: 1.65"
这个方法定义了一个指针类型为Person的方法GetInfo,用来返回一个包含Person对象信息的字符串。我们可以通过调用结构体变量的方法来实现对结构体对象的操作。这种使用方法就很棒!这就有点像类方法,GetInfo函数就是Person结构体的类方法。想要使用这个方法,那么就需要先构造一个Person的结构体对象,然后通过对象调用。
此外,Go还支持封装、继承、多态的特性,用来实现复杂的对象模型和数据结构。
- 封装
type Person struct {
name string
age int
}
func (p *Person) SetName(name string) {
p.name = name
}
func (p *Person) GetName() string {
return p.name
}
这个结构体定义了一个名为Person的结构体类型,包含了两个私有的成员变量name和age,以及两个公有的方法SetName和GetName,用来设置和获取name成员变量的值。不同于其它语言使用Public,Private定义公有和私有,Go使用编程规范来定义这个概念。变量名首字母大写代表公有,对外可见;变量名首字母小写代表私有,对外不可见。(经过实验,上面的说法是有一个大前提的。同一个包内,无论是公有变量还是私有变量,在任何地方都可以访问!!!!,只有在不同的包里,才有上面变量名大小写来控制可见性的说法。😣😣😣)Go的变量命名主要使用驼峰命名法,也算是约定俗成吧。
- 继承和组合
type Person struct {
name string
age int
}
type Student struct {
Person // 匿名嵌套Person结构体
id string
}
func (s *Student) SetId(id string) {
s.id = id
}
这个结构体定义了一个名为Student的结构体类型,通过匿名嵌套Person结构体,实现了从Person结构体继承了name和age成员变量和方法,并添加了一个id成员变量和SetId方法。这样,我们就可以通过Student结构体来访问和操作Person结构体的成员变量和方法。匿名嵌套是继承,不匿名就是组合的使用方法了。
- 接口多态
声明一个Shape类型的接口,该接口里定义了Area()函数。Rectangle和Circle实现了Shape类型接口里的Area()的方法,可以认定为是一个实现类。PrintArea方法接受一个Shape类型的数据,然后输出面积。这个形参是Shape类型,因此,就有了一个“向上转型”的效果。
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func PrintArea(s Shape) {
fmt.Println(s.Area())
}
func main() {
r := Rectangle{Width: 3, Height: 4}
c := Circle{Radius: 5}
PrintArea(r) // 输出 12
PrintArea(c) // 输出 78.53981633974483
}