Willson Chen

Stay Hungry, Stay Foolish.

Golang 基础(二)

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
}