文章目录
Golang 基础(二)
函数
定义与使用
Go 语言的函数定义语法如下:
func functionName(param1 type1, param2 type2, ...) (result1 type1, result2 type2, ...) {
//TODO
return result1, result2, ...
}
函数名特点:
- 函数名以大写字母开头,是公开函数,表示允许被其他包调用。
- 函数名以小写字母开头,是内部函数,仅限包内使用。
- 不支持函数重载(相同函数名使用不同参数类型)。
参数特点:
- 支持多个参数,或无参数。
- 一组类型相同的参数时,可以简写参数类型。
- 支持可变参数,用符号…表示参数个数不确定,其实际类型是切片。
- 值类型作为参数时,会发生值拷贝,如果要使用引用传递,需用指针。
func func1(){} //无参数
func func2(s string, x, y int){} //多个参数,x与y都为int类型,简写
func func3(args ...int){} //可变参数,调用时用 func3(1,2,3) 或 func3(slice...)
func func4(a *int){} //用指针实现引用传递
返回值特点:
- 支持多个返回值,或无返回值,多个返回值时需用括号。
- 使用命名返回值时,返回值在函数中赋值,且return 语句可以不带返回值。
- 命名返回值,如果是一组类型相同的返回值时,可以简写。
- 函数调用时,可以用 _ 忽略某个返回值。
func func1() int { return 0 } //单返回值
func func2() (int, string) { //多返回值
return 0, "" //必须带返回值
}
func func3() (x, y int, s string) {//多命名返回值,简写
x, y, s = 0, 1, ""
return //等同于 return x, y, s
}
函数是第一类值(或一等对象):
- 第一类值不是严格概念,通常指可以作为变量、参数、返回值等像基本类型变量一样使用的值。
- 比如python也支持函数作为第一类值,如c++则作为二等对象存在。
- 函数作为第一类值,可以作为参数传递给其他函数,可以作为返回值,可以赋值给变量。
func useFunc(a func(int)){} //函数本身作为参数
func buildFunc() func(string) { //函数本身作为返回值
return func(s string) {
fmt.Println(s)
}
}
//调用方法如:buildFunc()("hello")
特殊函数
- init()
- 在包初始化时执行,允许定义多个,都会被执行。
- 不能带参数和返回值。
- 用 import _ 引用一个包,就是为了执行该包的 init 函数。
- main()
- 只能在main包中定义一个。
- 不能带参数和返回值。
- 在本包和依赖包的所有 init() 函数执行完后才执行。
匿名函数
匿名函数是没有函数名的函数,应用场景:
- 赋值给变量、作为参数传递或作为函数返回值。
- 创建闭包。
func main(){
//函数变量
myfunc := func (s string){
fmt.Println(s)
}
myfunc("hello")
//并发执行
go func (s string){
fmt.Println(s)
}("hello")
}
闭包
- Go 支持在函数体内部定义函数,内部函数可以访问外部函数的局部变量。
- 内部函数与其外部环境变量的组合体就是闭包。
- 即使外部函数已经执行完了,其作用域的变量仍然作为闭包的一部分保留下来,可以延续访问。
- 闭包的使用场景:
- 延迟执行,通过 defer 关键字运行一个匿名函数,处理资源释放。
- 并发执行,通过 go 关键字运行一个匿名函数,函数内可以访问外部变量。
- 事件回调,当事件发生时就可以访问到所在环境的变量。
- 缓存计算结果。
func outer() func() int {
counter := 0
return func() int {
counter++
return counter
}
}
func main() {
//闭包
closure := outer()
fmt.Println(closure()) //1
fmt.Println(closure()) //2
}
//该例子中,outer()函数执行完后,counter局部变量作为闭包的一部分保留下来,仍然可以被读写。
递归
Go 支持递归,递归函数是指函数在内部直接或间接调用自身。
递归函数的特性:
- 函数内部调用自身。
- 函数内部必须要有退出条件,否则会陷入死循环。
defer 延迟执行
defer 语句用于延迟调用指定的函数。
defer 的特点:
- defer 语句的执行顺序与声明顺序相反。
- defer 是在返回值确定与 return 之间执行的。
- 如果是命名返回值,defer中可以修改到返回值。
defer 的使用场景:
- 释放资源,如打开的文件、数据库连接、网络连接等。
- 捕获panic,在发生异常时,defer语句可以捕获异常,并执行defer语句后的函数。
//文件释放
func openFile() {
file, err := os.Open("txt")
if err != nil {
return
}
defer file.Close() //合理位置
}
//锁释放
func lockScene() {
var mutex sync.Mutex
mutex.Lock()
defer mutex.Unlock()
//业务代码...
}
//捕获 panic
func demo() {
defer func() {
if err := recover(); err !=nil{
fmt.Println(string(Stack()))
}
}()
panic("unknown")
}
func a() (a int) {
defer func() { a++ }()
return a + 10
}
func b() (a int) {
defer func() { a = 2 }()
return a + 10
}
func main() {
fmt.Println(a(), b()) // 11 2
}
struct 结构体
struct 是自定义结构体,用于聚合多种类型的数据。
struct 的定义与使用
定义:
- 结构体的类型名在包内唯一。
- 字段名必须唯一。
- 同类型的字段可以简写在一行。
- 支持匿名结构体,用于临时使用。
- 支持嵌套结构体。
- 支持字段加 tag,再通过反射来获取,常用于序列化或 orm。
使用:
- 结构体是一种类型,像其他类型一样声明与实例化
- 初始化时可以直接对成员赋值,可以用字段名,也可以直接按字段顺序赋值。
- 结构体是值类型,会发生值拷贝。
- 支持用 new(T) 创建结构体指针。
- 无论实体还是指针,都用符号.访问其字段。
空struct作用:
- 作用是占位,表示某种不包含任何数据的类型,可以避免内存占用。
- 定义只包含方法的对象时,可以用空struct。
- 当map当只关心key是否存在时,值可以用空struct。
- 当通道只需要传递信号时,可以用空struct。
- Value Context的key使用空struct,既避免内存开销又能避免冲突。
结构体比较:
- 只有相同类型的结构体才可以比较,
- 结构体是否相同与字段的名称、类型、顺序都有关。
- 如果包含不可比较的字段,如slice/map/function,则无法比较。
type Point struct{ X, Y int } //X 与 Y 简写在一行
type Staff struct {
Id int `json:"Identity"` //加 tag 控制 json 序列化字段名
Name string
Address struct { //嵌套匿名结构体
Street string
City string
}
}
func main() {
p1 := Point{1, 2} //按字段顺序直接赋值
p2 := Point{X: 3, Y: 4} //按字段名赋值
fmt.Println(p1, p2) //{1 2} {3 4}
s := &Staff{ //获取指针,经逃逸分析会分配到堆
Name: "wills",
Address: struct {
Street string
City string
}{
Street: "123 St.",
City: "SHENZHEN",
},
}
s.Id = 1 //通过指针访问字段方式一样
data, _ := json.Marshal(s)
fmt.Println(string(data))
//{"Identity":1,"Name":"wills","Address":{"Street":"123 St.","City":"SHENZHEN"}}
}
struct 方法
Go 支持为 struct 定义方法,再通过 x.方法名() 的方式调用。
方法定义方式如下:
func (x T) 方法名(参数) (返回值) { //对类型 T 定义方法
}
func (x *T) 方法名(参数) (返回值) {//对类型 T 的指针定义方法
}
注意:
- 方法可以定义在类型或类型的指针上,两种方式都可以通过 x.方法名() 的方式调用。
- 定义在指针上时,方法体中可以修改实例的成员变量。
- 定义在类型上时,修改实例的成员变量会因为值拷贝而失效。
- 不能同时定义在指针和类型上,否则会编译失败。
type Point struct{ X, Y int }
func (p *Point) Add1() { p.X++; p.Y++ }
func (p Point) Add2() { p.X++; p.Y++ } //因为值拷贝修改无效
func main() {
p := Point{10, 20} //按字段顺序直接赋值
p.Add1() //p 的数据发生变更
fmt.Println(p) //{11 21}
p.Add2() //p 的数据不会发生变更
fmt.Println(p) //{11 21}
}
struct 嵌入
struct嵌入其他命名struct可以实现组合模式,嵌入其他匿名struct可以实现类似继承模式。
如果A嵌入了匿名的B和C,则可以通过A直接访问B和C的字段或方法,Go 会由浅至深地查找,找到则停止查找。
type B struct{ x, y int }
type C struct{ m, n int }
func (b *B) Add() { b.x++; b.y++ }
type A struct {//A嵌入匿名的 B 和 C
B
C
z int
}
func main() {
a := A{B{10, 11}, C{20, 21}, 30}
a.Add() //通过 A 直接访问 B 的方法
a.m = 25 //通过 A 直接访问 C 的字段
fmt.Println(a) //{{11 12} {25 21} 30}
}
指针
指针是用来保存变量内存地址的变量。有以下特点:
- 用 & 取地址,用 * 取值。
- 用 new 实现堆分配并创建指针。
- 数组名不是首元素指针。
- 指针不支持运算。
- 可用 unsafe 包打破安全机制来操控指针。
- 常量无法取指针,会编译失败。
Go指针的应用场景:
- 使用指针实现作为参数,实现引用传递。
- 使用指针实现返回值,避免大对象的值拷贝,会引发逃逸分析,可能改堆分配。
- 使用指针实现方法,实现对成员变量的修改。
- 实现链表、树等数据结构。
逃逸分析:
- 逃逸分析是指在编译期分析代码,决定是否需要将变量从栈分配改到堆分配。
- 指针和闭包都会引发逃逸分析。
- 使用命令输出分析结果:go build -gcflags ‘-m -l’ x.go
//以下例子目的在展示指针用法,不代表该场景下需用指针。
//小对象建议使用值传递和值返回,避免在堆上分配内存,因为堆分配开销较大,还需要通过 GC 回收内存。
type Point struct{ x, y int }
// 使用指针作为参数,实现引用传递。
// 使用指针实现方法,实现对成员变量的修改。
func (its *Point) Add(p *Point) {
its.x, its.y = its.x+p.x, its.y+p.y
}
// 使用指针作为返回值,会引发逃逸分析,返回值在堆上分配。
func buildPoint(x, y int) *Point {
return &Point{x, y}
}
func main() {
p := buildPoint(1, 2)
p.Add(&Point{3, 4})
fmt.Println(p) //&{4 6}
}
数组
数组是定长且有序的相同类型元素的集合。有以下特点:
- 数组的长度是数组类型的一部分,因此不同长度的数组是不同的类型。
- 数组在声明赋值时,可以用符号…借助编译器推断长度。
- 初始化时可以指定索引来初始化。
- 数组是值类型,赋值或传参时会发生值拷贝,要使用引用拷贝需用指针。
- 使用内建函数len()和cap()获取到的都是数组长度。
- 数组可以用 for range 来遍历,支持
- for i,v := range arr {} //i为索引、v为元素
- for i := range arr {} //i为索引
- for _,v := range arr {} //v为元素
- 支持多维数组,本质是数组的数组。
//方式一,先声明再赋值
var a [3]int
a = [3]int{1,2,3}
//方式二,用 var 声明且赋值
var b = [3]int{1, 2, 3}
//方式三,用 := 符号声明且赋值
c := [3]int{1, 2, 3}
//方式四,借助编译器推断长度
d := [...]int{1, 2, 3}
s := [...]string{0: "a", 3: "b"} //通过索引赋值,1 和 2 是默认值""
for index, val := range s { //index是索引,val 是元素值
if val == "" {
s[index] = "-" //注意 val 值复制修改无效,要通过索引修改数组
}
}
fmt.Println(len(s), s) //4 [a - - b]
//多维数组
arr := [2][3]int{{11, 12, 13}, {21, 22, 23}}
fmt.Println(arr) //[[11 12 13] [21 22 23]]
fmt.Println(len(arr)) //2
切片
切片是动态数组,是变长且有序的相同类型元素的集合。
切片的使用:
- 切片的声明与初始化与数组相似,但是不需要指定长度。
- 用 len()获取长度,用 cap()获取容量。
- 如果未初始化,值为 nil,长度为 0,容量为 0。
- 用 append() 函数可以动态的添加元素,添加元素可能导致切片扩容。
- 用 make() 函数初始化切片,可以指定长度和容量。
- 浅拷贝:切片是引用类型,赋值或传参时仅仅是复制引用。
- 深拷贝:要复制创建新切片需用 make()初始化新切片再用copy()复制数据。
- 用[上限:下限]语法可以截取子切片,包括上限但不包括下限,不指定上限或下限表示截取到头或尾。
- 切片可以用 for range 来遍历,类似数组。
- 支持多维切片,本质是切片的切片。
s1 := []int{0, 1, 2} //声明且初始化
s1 = append(s1, 3) //追加元素,append返回值必须赋值回切片
s2 := s1 //仅复制引用
s2[0] = 9 //s2和s1是同个切片的引用,修改s2也会修改到s1
fmt.Println("s1:", len(s1), cap(s1), s1) //s1: 4 6 [9 1 2 3]
var s3 []int //仅声明不初始化
fmt.Println("s3:", len(s3), cap(s3), s3 == nil) //s3: 0 0 true
s3 = []int{} //初始化空切片,空切片不等于 nil
fmt.Println("s3:", len(s3), cap(s3), s3 == nil) //s3: 0 0 false
s3 = make([]int, len(s1), 100) //初始化容量为 100
copy(s3, s1) //复制数据
s4 := append(s3, 4) //追加到新的切片,没有扩容 s4 和 s3 底层数组为同一个,但长度不同表示的数据仍然不同
s4[0] = 99 //会同时修改到s3和s4的第一个元素
fmt.Println("s3:", len(s3), cap(s3), s3) //s3: 4 100 [99 1 2 3]
fmt.Println("s4:", len(s4), cap(s4), s4) //s4: 5 100 [99 1 2 3 4]
s5 := s4[1:3] //截取索引1到2,不包括 3
fmt.Println("s5:", len(s5), cap(s5), s5) //s5: 2 99 [1 2]
s6 := s4[:3] //截取索引0到2,不包括 3
fmt.Println("s6:", len(s6), cap(s6), s6) //s6: 3 100 [99 1 2]
s7 := s4[3:] //截取索引3到尾部
fmt.Println("s7:", len(s7), cap(s7), s7) //s7: 2 97 [3 4]
s7[0] = 1000 //截取的子切片数据仍然在母切片上,故修改元素会修改到母切片
fmt.Println("s4:", len(s4), cap(s4), s4) //s4: 5 100 [99 1 2 1000 4]
map
map 是一种无序的键值对的集合,键是唯一的。
map 的使用:
- 声明时需要指定 key 和 value 的类型,可以同时做初始化。
- 可以比较的类型(除slice/map/function)都可以作为key。
- 用 make() 或直接赋值做初始化。
- 未初始化时,值为 nil,长度为0,无法使用,否则会panic。
- 用 len() 获取长度,没有容量不支持 cap()。
- 如果预估数据较多,make() 时可以指定 size,避免扩容。
- 通过 v,ok := m[k] 方式获取 key 对应的 value,ok 表示是否找到。
- 是引用类型,赋值或传参时仅仅是复制引用。
- 不支持用 copy() 复制数据,需遍历逐key复制。
- 可以用 for range 来遍历。
- for k,v := range m {} //k为键、v为值
- for k := range m {} //k为键
- for _,v := range m {} //v为值
- 元素是无序的,无法保证每次遍历 key 的顺序都是一样的。
- 支持用 delete() 删除某个 key,且可以在遍历中删除。
- 支持多层嵌套,本质是map的map。
- map 不是并发安全的,需用锁或 sync.Map。
var m1 map[string]int //只声明不初始化无法使用
// m1["a"] = 1 //panic
fmt.Println("m1,", len(m1), m1 == nil) //m1, 0 true
m1 = map[string]int{} //初始化为空则可以使用
fmt.Println("m1,", len(m1), m1 == nil) //m1, 0 false
m2 := make(map[string]int, 100) //用make初始化,设置初始大小 100
fmt.Println("m2,", len(m2), m2) //m2, 0 map[]
m3 := map[string]int{"a": 1, "b": 2} //直接赋值初始化
fmt.Println("m3,", len(m3), m3) //m3, 2 map[a:1 b:2]
m4 := m3 //仅复制引用
m4["c"] = 3 //m4和m3是同个map的引用,修改m4也会修改到m3
fmt.Println("m3,", len(m3), m3) //m3, 3 map[a:1 b:2 c:3]
for k := range m4 { //遍历key
if k == "b" {
delete(m4, k) //遍历中可以删除 key
}
}
_, ok := m4["b"] //ok表示 key 是否存在
fmt.Println("m4, b ok", ok) //m4, b ok false
mm := map[string]map[string]int{ //嵌套 map
"一": {"a": 10, "b": 11},
"二": {"m": 20, "n": 21},
}
fmt.Println("mm,", len(mm), mm) //mm, 2 map[一:map[a:10 b:11] 二:map[m:20 n:21]]
interfae 接口
- interfae 用于定义一组方法,只要结构体实现了这些方法,就实现了该接口。接口的作用在于解耦和实现多态。
- 接口可以嵌套多个其他接口,等于拥有了这些接口的特征。
- 空接口 interface{}(内建别名 any) 可以赋值为任意类型变量,结合类型判断或反射可以实现处理任意类型数据。
- 接口类型转换可以用类型断言,语法如 x,ok := value.(type)。
- 接口类型的变量可以用符号= =进行比较,只有都为 nil 或类型相同且值相等时才为 true。
type Phone interface {
Call(num string)
}
type Camera interface {
TakePhoto()
}
type SmartPhone interface { //嵌套了 Phone 和 Camera
Phone
Camera
}
type IPhone struct{}
func (iphone IPhone) Call(num string) {
fmt.Println("iphone call", num)
}
func (iphone IPhone) TakePhoto() {
fmt.Println("iphone take photo")
}
type Android struct{}
func (android Android) Call(num string) {
fmt.Println("android call", num)
}
func (android Android) TakePhoto() {
fmt.Println("android take photo")
}
type SmartPhoneFactory struct{}
func (factory SmartPhoneFactory) CreatePhone(phoneType string) SmartPhone {
switch phoneType {
case "iphone":
return IPhone{}
case "android":
return Android{}
default:
return nil
}
}
func main() {
sp := SmartPhoneFactory{}.CreatePhone("iphone")
sp.Call("123456") //iphone call 123456
sp.TakePhoto() //iphone take photo
var x interface{} = sp
switch x.(type) {
case IPhone:
fmt.Println("x is iphone")
case Android:
fmt.Println("x is android")
} //x is iphone
iphone, ok := x.(IPhone) //类型断言
fmt.Println(iphone, ok) //{} true
var value interface{} = 123
i, ok := value.(int)
fmt.Println(i, ok) //123 true
i32, ok := value.(int32) //常量 123 是 int 类型,类型断言失败
fmt.Println(i32, ok) //0 false
}