Go语言是门面向对象的编程语言
面向对象:化繁为简, 能不自己干自己就不干,关注的是我应该让谁来做?

变量

  • 变量名必须以字母开头,可以包含字母、数字和下划线。

  • 变量名不能以数字开头。

1
2
3
4
var 变量名 变量类型
var name string
var age int

1
2
3
4
5
var 变量名 类型 = 表达式
var a int = 10
var name string = "yy"
//多个变量声明
var x, y, z int = 1, 2, 3
1
2
3
4
5
6
7
8
9
//自动类型推断: Go 允许根据初始值推断变量类型
var a = 42 // 编译器推断 a 的类型为 int
var b = 3.14 // 编译器推断 b 的类型为 float64
var c = "hello" // 编译器推断 c 的类型为 string

//类型推断的前提是必须有初始化值。如果没有初始化值,编译器将无法推断类型。
//例如,以下代码会报错:
var a
// 错误:编译器无法推断类型 推断类型与字面值一致。
1
2
3
4
5
c := 3.14 //使用 := 来给变量赋值,仅限于函数内使用

d := 42 // 编译器推断 d 的类型为 int
e := 3.14 // 编译器推断 e 的类型为 float64
f := true // 编译器推断 f 的类型为 bool
1
2
3
4
5
6
7
//批量声明
var (
a string
b int
c bool
d float32
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//匿名变量

package main

import "fmt"

func main() {
// 使用匿名变量存储函数参数
var _ = "Hello, World!"
fmt.Println(_) // 打印匿名变量的值

// 使用匿名变量存储计算结果
var _, _ = 1, 2
fmt.Println(_, _) // 打印两个匿名变量的值
}

//由于匿名变量没有名字,它们不会占用命名空间,因此它们之间不存在重复声明的问题
//在 Go 中,匿名变量主要用于函数内部,以避免污染全局命名空间。

变量的零值

  • 整型

    • int 类型的零值是 0

    • uint 类型的零值是 0

  • 浮点型

    • float32 类型的零值是 0.0

    • float64 类型的零值是 0.0

  • 布尔型

    • bool 类型的零值是 false
  • 字符串

    • string 类型的零值是空字符串 ""
  • 指针

    • 指针类型的零值是 nil
  • 切片

    • 切片类型的零值是空切片 []
  • 映射

    • 映射类型的零值是空映射 map{}
  • 通道

    • 通道类型的零值是 nil
  • 结构体

    • 结构体类型的零值是零值是结构体的零初始化,例如 struct{}
  • 接口

    • 接口类型的零值是 nil

常量

常量用于存储固定不变的值,其值在程序运行期间不能改变。使用 const 关键字声明常量。

声明了pie这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了

1
2
const pi = 3.1415
const e = 2.7182

枚举常量

1
2
3
4
5
const (
A = iota // 0
B = iota // 1
C = iota // 2
)
1
2
3
4
5
6
const (
n1 = iota //0
n2 //1
_
n4 //3
)
1
2
3
4
5
6
7
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
const (
a = iota // 这是第一次使用 iota,所以 a = 0
b // 这是第二次使用 iota,所以 b = 1
c // 这是第三次使用 iota,所以 c = 2
d = "ha" // 这里没有使用 iota,所以 iota 的计数器不会增加
e // 这是第四次使用 iota,所以 e = 0(因为上一次赋值重置了 iota)
f = 100 // 这里没有使用 iota,所以 iota 的计数器不会增加
g // 这是第五次使用 iota,所以 g = 1(继续上一次的计数)
h = iota // 这里显式地使用 iota,所以 h = 7(因为这是新的一轮,从0开始,然后增加7次)
i // 这是第六次使用 iota,所以 i = 8
)
fmt.Println(a, b, c, d, e, f, g, h, i)
}

常量和变量在内存区域的存储

Go 内存区域划分

Go 语言中,内存通常分为三大区域:

  • 全局静态区:存储全局变量和常量。这些变量和常量在程序启动时分配内存,并在程序结束时释放。

就像是家里的储藏室,存放不常变动的东西。

  • 堆:存储动态分配的内存,通常用于需要在多个函数间共享的对象。堆上的数据需要手动管理或由 Go 的垃圾回收机制负责清理。

  • 栈:用于函数调用时

栈与堆的差异

栈和堆的差异

变量的内存存储

  • 全局变量:全局变量声明在函数外,属于静态存储区,存储在全局静态区中。这些变量在程序启动时被分配,程序结束时被释放。全局变量的生命周期与程序的生命周期相同,程序一启动就会分配存储空间,直到程序结束。

  • 局部变量:局部变量是在函数内部声明的,它们通常存储在栈上(如果不逃逸)。当函数调用时,栈帧会为局部变量分配内存;当函数返回时,这些栈内存会自动释放。如果局部变量逃逸到堆中,那么数据会存储在堆上。作用域:从定义哪一行开始直到与其所在的代码块结束

  • 指针变量:指针变量存储在栈上或堆上,但它们指向的数据可以位于堆中。如果变量逃逸到堆中,那么数据会存储在堆上。

常量的内存存储

  • 编译时常量:常量使用 const 关键字声明,它们通常在编译时确定,并且不会在运行时分配实际的存储空间。常量在内存中的表现方式与字面量类似,在很多情况下,编译器会直接将常量的值内联到使用它的地方。由于常量的值在编译时确定,因此它们不会像变量一样占用运行时内存。

  • 只读常量:对于某些复杂常量(例如字符串),它们会存储在只读的内存区,通常位于全局静态区中。尽管这些常量不会在运行时更改,它们可能仍然占用一些内存资源。

变量的内存存储

内存逃逸分析

在Go语言中,逃逸分析(escape analysis)是一种编译期优化技术,用于确定局部变量的存储位置。如果变量只在当前函数内部使用,并且没有被传递到其他函数或goroutine,那么这个变量通常存储在栈(stack)上。如果变量的生命周期超出了当前函数,比如被返回或传递给了其他goroutine,那么这个变量就会“逃逸”到堆(heap)上。

让我们用大白话来解释一下这个过程:

逃逸分析是什么?

逃逸分析就像是侦探工作,编译器像侦探一样,追踪每个局部变量的去向。如果变量只在当前函数内部使用,那么它就老老实实地呆在栈上。但如果变量被带出了当前函数,比如被函数返回,或者被发送到另一个goroutine,那么它就需要一个更长久的家——堆。

为什么变量会逃逸到堆上?
  1. 变量被返回

    • 如果一个变量的地址被返回,那么它可能被其他函数长时间持有,所以它需要存储在堆上,以免在函数返回后被销毁。
  2. 变量被传递给goroutine

    • 如果一个变量被传递给一个新的goroutine,那么它的生命周期就不再局限于当前函数,它需要在堆上分配内存,以确保在goroutine执行期间变量仍然有效。
  • 什么时候会发生

    • 如果局部变量的生命周期超出了它所在的函数,比如被函数返回或者被传递给了其他的goroutine,那么它就需要一个更长久的存储位置,这时候它就会“逃逸”到堆上。
  • 堆的作用

    • 堆就像是家里的储藏室,用来存放那些需要长时间保存的东西。在Go语言中,当局部变量逃逸到堆上时,就意味着它们被分配在堆上,可以被程序的其他部分长期访问。
1
2
3
4
func escapeExample() *int {
var x int = 42 // x 在栈上分配
return &x // x 的地址被返回,x 逃逸到堆上
}

Go数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main
import "fmt"
func main() {
var intV int // 整型变量
var floatV float32 // 实型变量
var boolV bool // 布尔型变量
var stringV string // 字符串变量
var pointerV *int // 指针变量
var funcV func(int, int)int // function变量
var interfaceV interface{} // 接口变量
var sliceV []int // 切片变量
var channelV chan int // channel变量
var mapV map[string]string // map变量
var errorV error // error变量

fmt.Println("int = ", intV) // 0
fmt.Println("float = ", floatV) // 0
fmt.Println("bool = ", boolV) // false
fmt.Println("string = ", stringV) // ""
fmt.Println("pointer = ", pointerV) // nil
fmt.Println("func = ", funcV) // nil
fmt.Println("interface = ", interfaceV) // nil
fmt.Println("slice = ", sliceV) // []
fmt.Println("slice = ", sliceV == nil) // true
fmt.Println("channel = ", channelV) // nil
fmt.Println("map = ", mapV) // map[]
fmt.Println("map = ", mapV == nil) // true
fmt.Println("error = ", errorV) // nil

var arraryV [3]int // 数组变量
type Person struct{
name string
age int
}
var structV Person // 结构体变量
fmt.Println("arrary = ", arraryV) // [0, 0, 0]
fmt.Println("struct = ", structV) // {"" 0}
}

复合数据类型

数组(Array)

数组是具有固定长度的同类型元素集合。一旦定义,数组的长度就固定,不能改变。

1
2
3
var arr [5]int = [5]int{1, 2, 3, 4, 5} // 定义一个长度为5的整型数组
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
fmt.Println(len(arr)) // 输出数组长度

如果数组长度不确定,可以使用 … 代替数组的长度,编译器会根据元素个数自行推断数组的长度:

1
2
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

遍历数组

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
func main() {
arr := [...]int{1, 3, 5}
// 传统for循环遍历
for i:=0; i<len(arr); i++{
fmt.Println(i, arr[i])
}
// for...range循环遍历
for i, v := range arr{
fmt.Println(i, v)
}
}

二维数组

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main() {
// 创建一个两行三列数组
arr := [2][3]int{
{1, 2, 3},
{4, 5, 6}, //注意: 数组换行需要以逗号结尾
}
fmt.Println(arr)// [[1 2 3] [4 5 6]]
}
1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main() {
// 创建一个两行三列数组
arr := [...][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(arr)// [[1 2 3] [4 5 6]]
}

切片(Slice)

  • 切片是基于数组的动态数组,长度可以变化,切片不需要指定长度,可以根据需求动态增减长度。

  • 切片是引用类型,它是对底层数组的引用,因此修改切片会影响底层数组的内容。

  • 切片由三个部分组成:指向底层数组的指针、切片的长度和切片的容量。

1
2
3
4
var s []int // 定义一个整数切片
s := []int{1, 2, 3, 4, 5} // 定义并初始化切片
s[0] = 10 // 修改第一个元素
fmt.Println(s[0]) // 访问第一个元素
1
2
var slice []int = []int{1, 2, 3}
slice = append(slice, 4) // 动态扩展切片
1
2
3
s1 := []int{1, 2, 3}
s2 := make([]int, len(s1))
copy(s2, s1) // 复制 s1 的内容到 s2

make函数创建 make(类型, 长度, 容量)

  • 内部会先创建一个数组, 然后让切片指向数组

  • 如果没有指定容量,那么容量和长度一样

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
func main() {
var sce = make([]int, 3, 5)
fmt.Println(sce) // [0 0 0]
fmt.Println(len(sce)) // 3
fmt.Println(cap(sce)) // 5
/*
内部实现原理
var arr = [5]int{0, 0, 0}
var sce = arr[0:3]
*/
}
1
2
3
4
5
6
var slice4 []int = []int{1,2,3,4,5}
var slice5 = make([]int,10)copy(slice5,slice4) //将切片slice4拷贝为slice5

fmt.Println(slice4) //1,2,3,4,5
fmt.Println(slice5) //1,2,3,4,5,0,0,0,0,0
//默认情况下,使用make后,多余的空间默认为0
1
2
3
4
5
6
type slice struct{
array unsafe.Pointer // 切片的底层实现是指向数组的指针
len int // 切片长度(保存了多少个元素)
cap int // 切片容量(可以保存多少个元素)
}
//cap 总是大于等于 len
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import("fmt")
func main(){
var intArr [5]int = [...]int{11,22,33,44,55} //数组
slice := intArr[1:3]
fmt.Println("intarr=",intArr) //intArr= [11 22 33 4 55]
fmt.Println("intarr的容量是 ",len(inArr)) //intArr的容量是 5
fmt.Println("slice 的元素是 ",slice) //slice 的元素是 [22 33]
//数组的第二个元素(下标为 1)到第三个元素(下标为 3)的切片。
//切片不包含第四个元素(下标为 3)
fmt.Println("slice 的容量是",cap(slice)) //slice 的容量是 2
//切片的容量是可变的,这意味着你可以在切片的生命周期内增加切片的长度。
//切片的长度是不可变的,但是你可以在切片上添加或删除元素,从而改变切片的长度。
fmt.Println("slice 的元素个数为",len(slice)) //slice 元素个数为 2

}
1
2
3
4
//切片可以被进一步切片,产生新的切片
s := []int{1, 2, 3, 4, 5}
s1 := s[1:4] // s1 为 [2, 3, 4]
s2 := s[2:] // s2 为 [3, 4, 5]
1
2
3
4
5
6
7
8
//切片可以作为函数参数,并可以直接在函数内部修改。由于切片是引用类型,函数内的修改会影响原切片
func modifySlice(s []int) {
s[0] = 100 // 修改切片的第一个元素
}

s := []int{1, 2, 3}
modifySlice(s)
fmt.Println(s) // 输出 [100, 2, 3]

字典(Map)

字典是一种键值对数据结构,用于高效地存储和检索元素。

1
2
//map格式:var dic map[key数据类型]value数据类型
var m map[string]int = map[string]int{"foo": 1, "bar": 2}

map的增删改查

  • 增加

map 中没有指定的键时,就会自动增加键值对。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var dict = make(map[string]string)
fmt.Println("增加前:", dict) // map[]
dict["name"] = "ln"
fmt.Println("增加后:", dict) // map[name:ln]
}
  • 修改

map 中有指定的键时,就会自动修改键对应的值。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var dict = map[string]string{"name": "ln", "age": "3", "gender": "male"}
fmt.Println("修改前:", dict) // map[name:ln age:3 gender:male]
dict["name"] = "zs"
fmt.Println("修改后:", dict) // map[name:zs age:3 gender:male]
}
  • 删除

可以通过 Go 语言内置的 delete 函数删除指定键的元素。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
var dict = map[string]string{"name": "ln", "age": "3", "gender": "male"}
fmt.Println("删除前:", dict) // map[name:ln age:3 gender:male]
// 第一个参数: 被操作的字典
// 第二个参数: 需要删除元素对应的键
delete(dict, "name")
fmt.Println("删除后:", dict) // map[age:3 gender:male]
}
  • 查询

通过 ok-idiom 模式判断指定键值是否存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
var dict = map[string]string{"name": "ln", "age": "3", "gender": "male"}
// value, ok := dict["age"]
//if(ok){
// fmt.Println("有age这个key,值为", value)
//}else{
// fmt.Println("没有age这个key,值为", value)
// }
if value, ok := dict["age"]; ok{
fmt.Println("有age这个key,值为", value)
fmt.Println("没有age这个key,值为", !ok)
}
  • 遍历

遍历 map 时,map 中存储的数据是无序的,所以多次输出的顺序可能不同。

1
2
3
4
var dict = map[string]string{"name": "ln", "age": "3", "gender": "male"}
for key, value := range dict {
fmt.Println(key, value)
}

结构体(Struct)

结构体是一种复合数据类型,用于将不同类型的数据组合在一起。

1
2
3
4
5
type Person struct {
Name string
// 结构体类型中的字段
Age int
}
1
2
3
4
5
6
p := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
fmt.Println(p.FirstName) // 输出: John

指针(Pointer)

指针存储变量的内存地址。Go 不支持指针运算,但可以通过指针来改变原变量的值。

1
2
3
var x int = 10
var p *int = &x // p 存储 x 的内存地址
*p = 20 // 修改 x 的值为 20

特殊数据类型

接口(Interface)

接口定义了一组方法,而不实现这些方法。实现接口的具体类型必须实现接口中定义的所有方法。

1
2
3
type Speaker interface {
Speak()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main
import "fmt"
// 1.定义一个接口
type usber interface {
start()
stop()
}
type Computer struct {
name string
model string
}
// 2.实现接口中的所有方法
func (cm Computer)start() {
fmt.Println("启动电脑")
}
func (cm Computer)stop() {
fmt.Println("关闭电脑")
}

type Phone struct {
name string
model string
}
// 2.实现接口中的所有方法
func (p Phone)start() {
fmt.Println("启动手机")
}
func (p Phone)stop() {
fmt.Println("关闭手机")
}

// 3.使用接口定义的方法
func working(u usber) {
u.start()
u.stop()
}
func main() {
cm := Computer{"戴尔", "F1234"}
working(cm) // 启动电脑 关闭电脑

p := Phone{"华为", "M10"}
working(p) // 启动手机 关闭手机
}

在Go语言中,接口是一种类型,它定义了一组方法。如果一个类型实现了接口中声明的所有方法,我们就说这个类型实现了这个接口。多态就是通过接口来实现的,它允许你使用接口类型的变量来引用任何实现了该接口的具体类型。

接口定义行为:接口是方法的集合,定义了行为的抽象。

类型自动实现接口:Go 没有显式的 implements 关键字,只要类型实现了接口中的方法,它就自动被视为该接口类型。

多态性:通过接口,Go 实现了多态性。我们可以通过接口类型变量来调用不同实现的类型。

接口解耦:接口将高层逻辑和具体实现分离,易于扩展和维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
)
//第一步:定义接口(行为描述)
// Animal接口,包含一个Speak方法
//Speak 是接口的方法签名,表示所有 Animal 都可以发出声音
type Animal interface {
Speak() string // Speak方法返回一个字符串
}

//第二步:定义实现接口的具体类型
// Dog 结构体,代表狗
type Dog struct{}

// Cat 结构体,代表猫
type Cat struct{}

//只要它们定义了 Speak 方法,Go 就会自动识别它们为 Animal 类型
// Dog 实现了 Animal 接口的 Speak 方法
func (d Dog) Speak() string {
return "Woof!" // 狗叫声
}

// Cat 实现了 Animal 接口的 Speak 方法
func (c Cat) Speak() string {
return "Meow!" // 猫叫声
}

//第三步:使用接口实现多态
// MakeSound 接收一个 Animal 类型的参数,并调用其 Speak 方法
func MakeSound(a Animal) {
fmt.Println(a.Speak())
}

func main() {
dog := Dog{} // 创建Dog实例
cat := Cat{} // 创建Cat实例

// 调用MakeSound时,可以传入任何实现了Animal接口的类型
MakeSound(dog) // 输出:Woof!
MakeSound(cat) // 输出:Meow!
}

1
2
Woof!
Meow!

进阶示例:增加更多的实现类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义 Cow 结构体,代表牛
type Cow struct{}

// 定义 Duck 结构体,代表鸭
type Duck struct{}

// 实现 Cow 的 Speak 方法
func (c Cow) Speak() string {
return "Moo!" // 牛的叫声
}

// 实现 Duck 的 Speak 方法
func (d Duck) Speak() string {
return "Quack!" // 鸭的叫声
}

func main() {
// 创建一个包含不同 Animal 类型的切片
animals := []Animal{Dog{}, Cat{}, Cow{}, Duck{}}

// 遍历 animals 切片,调用每个 Animal 的 Speak 方法
for _, animal := range animals {
MakeSound(animal)
}
}
1
2
3
4
Woof!
Meow!
Moo!
Quack!

面向对象设计思想与接口的关系

在面向对象设计中,接口扮演了模块间的“通信契约”,它帮助我们分离具体实现和高层逻辑:

  1. 抽象和封装:接口提供了一种抽象,定义了一组方法,而不关心方法的具体实现。每个实现接口的类型可以有不同的内部逻辑,但对外表现一致。

  2. 多态性:接口允许我们在不同类型之间实现多态,即用同一个接口类型的变量来操作不同的具体类型。在本例中,Animal 接口实现了多态性,使得 DogCatCowDuck 都可以作为 Animal 来使用。

  3. 解耦和扩展性:接口使代码更加解耦。调用者只需要知道接口,而不需要关心具体实现,符合“依赖于抽象而不依赖于具体”的设计原则。这种模式特别适合需要不断扩展的新类型需求。

类型断言

用于检查接口变量(interface)中实际存储的值的类型,并在检查的同时进行类型转换。

  1. 检查接口变量中存储的具体类型:当你不确定接口变量中存储的是哪个具体类型时,可以使用类型断言来检查。

  2. 从接口变量中提取具体类型的值:接口变量只能调用接口中定义的方法,如果你需要调用具体类型的方法,可以使用类型断言来提取具体的值。

1
2
双值形式:t, ok := i.(Type)
单值形式:t := i.(Type),如果类型不匹配,会直接引发 panic
1
2
3
4
5
i 是接口变量。
Type 是要断言的具体类型。
t 是断言成功后的具体类型的变量。
ok 是一个布尔值,表示断言是否成功。
如果 i 的实际类型与 Type 相同,那么 t 会是 Type 类型的变量,ok 为 true。如果类型不匹配,则 ok 为 false,t 的值为零值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
var a Animal = Dog{}

// 双值形式,安全判断类型
dog, ok := a.(Dog)
if ok {
fmt.Println("a 是 Dog 类型:", dog.Speak())
} else {
fmt.Println("a 不是 Dog 类型")
}

// 单值形式,不建议使用
// 如果 a 不是 Cat 类型,会触发 panic
cat := a.(Cat) // 这里会触发 panic,因为 a 不是 Cat 类型
fmt.Println("a 是 Cat 类型:", cat.Speak())
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
)

// Animal接口,所有动物都可以Speak
type Animal interface {
Speak() string
}

// Dog 结构体,实现了 Animal 接口
type Dog struct{}

func (d Dog) Speak() string {
return "Woof!"
}

// Cat 结构体,实现了 Animal 接口
type Cat struct{}

func (c Cat) Speak() string {
return "Meow!"
}

func main() {
var a Animal
a = Dog{} // 将 Dog 实例赋值给接口变量 a

// 类型断言:检查 a 是否是 Dog 类型
dog, ok := a.(Dog)
if ok {
fmt.Println("a 是 Dog 类型,叫声是:", dog.Speak())
} else {
fmt.Println("a 不是 Dog 类型")
}

// 再次断言 a 是否是 Cat 类型
cat, ok := a.(Cat)
if ok {
fmt.Println("a 是 Cat 类型,叫声是:", cat.Speak())
} else {
fmt.Println("a 不是 Cat 类型")
}
}

1
2
a 是 Dog 类型,叫声是: Woof!
a 不是 Cat 类型

type switch:简化多种类型断言

1
2
3
4
5
6
7
8
9
10
11
switch v := x.(type) {
case T1:
// 如果 x 存储的值是类型 T1,执行这里的代码
case T2:
// 如果 x 存储的值是类型 T2,执行这里的代码
case T3:
// 如果 x 存储的值是类型 T3,执行这里的代码
// ...
default:
// 如果 x 存储的值不匹配以上任何类型,执行这里的代码
}
1
2
3
x 是一个接口类型的变量。
v := x.(type) 是 type switch 的声明部分,v 是一个新的变量,它将存储 x 中实际存储的值。
T1、T2、T3 等是你要检查的类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func DescribeAnimal(a Animal) {
switch v := a.(type) {
case Dog:
fmt.Println("这是一只狗,它的叫声是:", v.Speak())
case Cat:
fmt.Println("这是一只猫,它的叫声是:", v.Speak())
default:
fmt.Println("未知类型的动物")
}
}

func main() {
DescribeAnimal(Dog{}) // 输出:这是一只狗,它的叫声是: Woof!
DescribeAnimal(Cat{}) // 输出:这是一只猫,它的叫声是: Meow!
}

函数(Function)

函数在 Go 中也是一种数据类型,可以作为参数或返回值传递给其他函数。

1
2
3
func add(a, b int) int {
return a + b
}

通道(Channel)

通道是 Go 语言中的一种用于并发的类型,用于在 goroutine 之间传递数据。

你在一家餐厅里,餐厅里有厨房和餐桌。厨师们在厨房里做好菜,然后需要把菜送到顾客的餐桌上。但是,我们不能让厨师直接冲到顾客面前送菜,这样会乱套的。所以,餐厅里有一种叫做“传菜口”的东西,厨师把菜通过传菜口递出来,服务员从传菜口取走菜,然后送到顾客的桌子上。

在Go语言里,通道(Channel)就有点像这个“传菜口”。它是一个让不同的工作线程(我们叫它们“goroutine”)之间传递消息和数据的通道。这些goroutine就像是厨师和服务员,它们不能直接交换东西,而是通过通道来传递。

通道的几个特点:

  1. 类型安全:就像传菜口只能递菜一样,Go语言中的通道也只能传递一种类型的数据。

  2. 同步:通道保证了数据的发送和接收是同步的。就像厨师把菜放进传菜口,必须有服务员来取走,才能继续做菜。

  3. 缓冲:有些通道可以存一些数据,就像传菜口可以暂时放几盘菜一样。如果通道里已经存满了菜,厨师就得等服务员取走一些,才能放下一盘新菜。

  4. 关闭:就像餐厅打烊后,厨师不会再做菜,通道也可以关闭,表示不会再有新的数据发送进来了。

为什么需要通道:

在Go语言里,我们经常需要同时做几件事情(并发),就像餐厅里同时有几桌顾客点菜一样。通道就是确保这些同时进行的事情能够协调工作,不会乱套的一种方式。它不仅让数据传递变得有序,还能保证数据的安全,因为数据只能在通道里传递,不会被别的程序或者goroutine乱改。

通道有 发送(send)、接收(receive)和 关闭(close)三种操作。

发送和接收都使用<-符号。

无缓冲的channel在发送和接收数据时是同步的:

  1. 发送操作:当一个goroutine向无缓冲的channel发送数据时,它必须等待另一个goroutine来接收这个数据,否则发送操作会一直阻塞。

  2. 接收操作:同样地,当一个goroutine从无缓冲的channel接收数据时,它也必须等待另一个goroutine发送数据,否则接收操作会一直阻塞。

  3. 同步:无缓冲的channel确保了发送和接收操作的同步性,即数据的发送和接收是严格交替进行的。

  4. 使用场景:无缓冲的channel常用于需要严格同步操作的场景,比如协调两个goroutine的工作流程。

1
2
3
4
5
6
7
8
9
10
11
package main
import ( "fmt")
func main() {
ch := make(chan int) // 无缓冲区的 channel
go func() { //启动了一个匿名的goroutine,在这个goroutine中,我们向channel ch 发送了数字 42
ch <- 42 // 发送数据会阻塞,直到有接收者
}()
// 接收数据
value := <-ch
fmt.Println("接收到的值:", value) // 输出: 接收到的值: 42,解除阻塞
}

有缓冲的channel(类比电话留言箱,无需对方立即接听)允许在没有接收者的情况下发送一定数量的数据,这个数量就是channel的缓冲大小:

  1. 发送操作:如果缓冲区未满,发送数据到有缓冲的channel不会阻塞,数据会被放入缓冲区中。只有当缓冲区满了,发送者才会阻塞,直到缓冲区有空间。

  2. 接收操作:如果缓冲区不为空,从有缓冲的channel接收数据不会阻塞,因为可以直接从缓冲区中取出数据。只有当缓冲区空了,接收者才会阻塞,直到有数据发送到channel。

  3. 异步:有缓冲的channel允许一定程度的异步操作,因为数据可以先存储在缓冲区中,不必立即被接收。

  4. 使用场景:有缓冲的channel适用于生产者-消费者问题,其中生产者可以快速地将数据放入缓冲区,而消费者则可以异步地从缓冲区中取出数据

  5. 注意:在关闭 channel 后,不要再向其发送数据,这样会引发 panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import "fmt"
func main() {
tasks := make(chan int, 10) // 创建一个缓冲大小为10的channel

for i := 0; i < 10; i++ {
fmt.Println("task ", i)
tasks <- i // 向channel发送数据。不阻塞,缓冲区有空间
}

close(tasks) // 发送完毕后关闭channel,不再有数据发送,可以结束接收循环了。

for task := range tasks {
fmt.Println("处理任务:", task) // 从channel接收并处理数据
}
}

数据类型转换

Go 是静态类型语言,必须显式地进行类型转换,不能像某些动态语言一样自动进行隐式类型转换。

1
2
var a int = 10
var b float64 = float64(a) // int 转换为 float64

运算符

  • Go语言中++、–运算符不支持前置

    • 错误写法: ++i; –i;
  • Go语言中++、–是语句,不是表达式,所以必须独占一行

    • 错误写法: a = i++; return i++;
1
2
3
4
5
6
7
8
9
10
11
12
a := 6        // 110 in binary
b := 3 // 011 in binary
fmt.Println(a & b) // 结果为2 (010 in binary)
//只有当两个操作数的对应位都为1时,结果位才为1。因此,结果为 010(十进制下为2)
fmt.Println(a | b) // 结果为7 (111 in binary)
//只要两个操作数的对应位有一个为1,结果位就为1。因此,结果为 111(十进制下为7)
fmt.Println(a ^ b) // 结果为5 (101 in binary)
//只要两个操作数的对应位不同,结果位就为1。因此,结果为 101(十进制下为5)
fmt.Println(a << 1) // 结果为12 (1100 in binary)
//a << 1 将 a 的二进制表示 110 向左移动1位,相当于乘以2的1次方。因此,结果为 1100(十进制下为12
fmt.Println(b >> 1) // 结果为1 (001 in binary)
//b >> 1 将 b 的二进制表示 011 向右移动1位,相当于除以2的1次方。因此,结果为 001(十进制下为1)
1
2
3
4
5
6
7
8
9
10
11
12
  package main

import "unsafe"const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a))

func main(){
println(a, b, c)
}
//以上实例运行结果为:
abc 3 16

流程控制

常见的分支控制语句包括 if、else、switch、select

  • for 循环:Go 中的循环结构主要通过 for 语句实现。Go 没有像其他语言中的 whiledo-while 循环,而是用 for 语句可以完成所有的循环需求。
1
2
3
4
5
6
for i := 0; i < 5; i++ {
if i == 3 {
continue // 跳过 i 等于 3 的那次循环
}
fmt.Println(i)
}
  • for…range 循环for...range 循环用于遍历数组、切片(slice)、map、字符串等集合类型的数据。
1
2
3
4
nums := []int{1, 2, 3, 4, 5}
for index, value := range nums {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}
  • select 语句:是 Go 特有的,用于处理多个通道(channel)的通信操作。它类似于 switch,但每个 case 都必须是一个通道操作。
1
2
3
4
5
6
7
8
9
10
select {
case <- channel1:
// 读取 channel1 的数据,channel1准备好了
case data := <- channel2:
//用 data 去接收数据
case channel3 <- data:
// 往 channel3 中写入数据
default:
// 没有任何channel准备好了
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"time"
)

func main() {
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(2 * time.Second)
ch1 <- "from ch1" //第一个goroutine在2秒后向ch1发送字符串"from ch1"
}()

go func() {
time.Sleep(1 * time.Second)
ch2 <- "from ch2" //第二个goroutine在1秒后向ch2发送字符串"from ch2"
}()

select {
case msg1 := <-ch1:
fmt.Println(msg1) //第一个goroutine在2秒后向ch1发送字符串"from ch1"
case msg2 := <-ch2:
fmt.Println(msg2) //第二个goroutine在1秒后向ch2发送字符串"from ch2"
case <-time.After(3 * time.Second): //超时,time.After(3 * time.Second)表示如果3秒内没有其他事件发生,则触发超时
fmt.Println("timeout")
}
}

//由于ch2的goroutine会在1秒后发送消息,这比ch1的goroutine和超时时间都要早
//所以select会首先执行ch2的case,打印出"from ch2"
  • 循环配合 defer 使用:在循环中,defer 语句会在当前函数返回之前按照后进先出的顺序执行。通常用于在循环中执行一些延迟操作比如文件关闭、资源释放等。
1
2
3
for i := 0; i < 3; i++ {
defer fmt.Println("延迟执行:", i)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
func main() {
defer func1()
defer func2()
defer func3()
}

func func1() {
fmt.Println("A")
}

func func2() {
fmt.Println("B")
}

func func3() {
fmt.Println("C")
}
1
2
3
C
B
A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func deferFunc() int {
fmt.Println("defer func called")
return 0
}

func returnFunc() int {
fmt.Println("return func called")
return 0
}

func returnAndDefer() int {
defer deferFunc() // 这将延迟执行deferFunc,直到本函数返回
return returnFunc() // 调用returnFunc并返回其结果
}

func main() {
returnAndDefer() // 调用returnAndDefer,但不接收其返回值
}
1
2
3
return func called
defer func called
//return语句总是先于defer语句执行
  • 嵌套循环:Go 允许在循环内嵌套其他循环,以实现更复杂的逻辑。
1
2
3
4
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
fmt.Printf("i: %d, j: %d\n", i, j)
}

在 Go 语言中,字符串是由两部分组成的:

  1. 字符串的长度:告诉我们字符串包含多少个字符。

  2. 指向字符串内容的指针:指向存储字符串实际字符数据的地方。

  • 指针:它指向字符串数据的实际存储位置。通常,在 64 位系统中,这个指针占用 8 字节。

  • 长度:这是一个整数,用来表示字符串的长度。在 64 位系统中,整数类型通常占用 8 字节。

因此,字符串的总大小计算如下:

  • 指针(8 字节)

  • 长度(8 字节)

8 字节 (指针)+8 字节 (长度)=16 字节


本站由 GINA 使用 Stellar 1.29.1 主题创建。

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

Copyright © 2024 GINA 保留所有权利。