Go语言速成指南(二)


Go 语言速成指南(二)

把所有的代码跟着敲一遍,基本语法全部能掌握🫡

代码仓库:https://github.com/Cidyerlia/learnGo/tree/master

切片

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

}

文章作者: 嘉沐
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 嘉沐 !
  目录