【go学习笔记三】map、函数、闭包、指针、结构体


golang map

赋值、访问

key value键值对的数据结构容器。内部实现是哈希表。引用类型。

声明方式:

var 变量名 map[变量类型] 值类型

声明之后不可直接用,需要初始化才能用。

例子:

func main() {
    m1 := make(map[string]string)
    m1["name"] = "hqinglau"
    m1["site"] = "orzlinux.cn"
    m1["email"] = "hqinglau@gmail.com"

    fmt.Printf("m1: %#v\n", m1)
    // m1: map[string]string{
    //    "email":"hqinglau@gmail.com", 
    //    "name":"hqinglau", 
    //    "site":"orzlinux.cn"
    //}
    
    
    m2 := map[string]string{
        "email": "hqinglau@gmail.com",
        "name":  "hqinglau",
        "site":  "orzlinux.cn",
    }
    fmt.Printf("m2[\"email\"]: %v\n", m2["email"]) // m2["email"]: hqinglau@gmail.com
}

判断键是否存在

m2 := map[string]string{
    "email": "hqinglau@gmail.com",
    "name":  "hqinglau",
    "site":  "orzlinux.cn",
}
v, ok := m2["age"]         // 不存在
fmt.Printf("v: %#v\n", v)  // 空串
fmt.Printf("ok: %v\n", ok) // false

遍历

for range

遍历key或者遍历key, value

m1 := map[string]string{
    "email": "hqinglau@gmail.com",
    "name":  "hqinglau",
    "site":  "orzlinux.cn",
}
for k := range m1 {
    fmt.Printf("%v: %v\n", k, m1[k])
}
fmt.Println("========================")
for k, v := range m1 {
    fmt.Printf("%v: %v\n", k, v)
}

golang函数

  • 三种函数:普通函数,匿名函数,方法(struct上的函数)
  • 不允许重载,也就是不允许函数同名
  • 函数不能嵌套函数,但是可以嵌套匿名函数
  • 函数可以赋值给一个变量,让变量也变成函数
  • 函数可以作为参数传递
  • 函数的返回值也可以是一个函数
  • 函数传参是拷贝的过程
  • 函数参数可以没有名字

定义

func 函数名 (参数) 返回类型 {
    
}

返回类型和参数都可以为空。

示例:

func sum(a int, b int) (ret int) {
    ret = a + b
    return ret
}

func hello(name string) {
    fmt.Printf("Hello, %v\n", name)
}

func main() {
    fmt.Printf("sum(1, 2): %v\n", sum(1, 2)) // sum(1, 2): 3
    hello("hqinglau")                        // Hello, hqinglau
}

返回值

可以有零个,一个,或多个。

func add_sub(a int, b int) (int, int) {
    return a + b, a - b
}

func main() {
    c, d := add_sub(2, 4)
    fmt.Printf("c,d: %v %v\n", c, d) // c,d: 6 -2
}

返回值里面有的有名字,有的没有,区别何在?

区别在于有名字的话,就不用return名字了,例如:

func add_sub1(a int, b int) (sum int, dif int) {
    sum = a + b // 此处相当于被声明过了,不需要 := 了。
    dif = a - b
    return
    // return sum dif // 不省略也可以
}

func add_sub2(a int, b int) (int, int) {
    sum := a + b
    dif := a - b
    return sum, dif
}

func main() {
    c, d := add_sub1(2, 4)
    fmt.Printf("c,d: %v %v\n", c, d) // c,d: 6 -2
    c, d = add_sub2(2, 4)
    fmt.Printf("c,d: %v %v\n", c, d) // c,d: 6 -2
}

函数参数

0,1,多个。

形参:声明函数时的参数列表

实参:调用时传递的参数列表

go语言通过传值的方式传参。传递给函数的是拷贝后的副本。可以使用变长参数。

多种类型传参测试

func test(a int, b string, c map[int]string, d [2]int, e []int) {
    a = 999
    b = "another"
    c[1] = "change"
    d[0] = 999
    e[0] = 999
}

func main() {
    i1 := 2
    i2 := "test"
    i3 := map[int]string{
        1: "tt",
    }
    i4 := [...]int{1, 2}
    i5 := []int{1, 2}

    test(i1, i2, i3, i4, i5)
    fmt.Printf("i1: %v\n", i1) // i1: 2
    fmt.Printf("i2: %v\n", i2) // i2: test
    fmt.Printf("i3: %v\n", i3) // i3: map[1:change]
    fmt.Printf("i4: %v\n", i4) // i4: [1 2]
    fmt.Printf("i5: %v\n", i5) // i5: [999 2]
}

分析

数字,数组都是直接内存拷贝。

字符串相当于i2指向test字符串的内存,然后传参拷贝了一份bb也指向test字符串的内存,但是之后更改了b的值,b改为指向another字符串的值,但是不影响i2仍然指向test字符串。

map和切片都是引用类型,实际上也都是指向的同一块内存,在上面做修改罢了。

可变长参数

func test(args ...int) {
    fmt.Println("-----------------")
    for _, v := range args {
        fmt.Printf("v: %v\n", v)
    }
}

func main() {
    test()
    test(1)
    test(1, 2, 3, 4, 5)
}
// -----------------
// -----------------
// v: 1
// -----------------
// v: 1
// v: 2
// v: 3
// v: 4
// v: 5

函数类型和函数变量

type关键字来定义函数类型,如:

type fun func(int,int) int

定义了一个fun函数类型,接收两个int类型的参数并返回一个int类型的返回值。

使用:

func sum(a int, b int) int {
    return a + b
}

func max(a int, b int) int {
    if a > b {
        return a
    } else {
        return b
    }
}

type fun func(int, int) int

func main() {
    var f fun
    f = sum
    s := f(1, 2)
    fmt.Printf("s: %v\n", s) // 3

    f = max
    s = f(1, 2)
    fmt.Printf("s: %v\n", s) // 2
}

高阶函数

函数作为另一个函数的参数,来进行参数传递,也可以作为返回值。

函数作为参数

func sayHello(name string) {
    fmt.Printf("Hello, %s\n", name)
}

func sayBye(name string) {
    fmt.Printf("Bye, %s\n", name)
}

func f1(name string, f func(string)) {
    f(name)
}

func main() {
    f1("tom", sayHello) // Hello, tom
    f1("tom", sayBye)   // Bye, tom
}

函数作为返回值

func add(x, y int) int {
    return x + y
}

func sub(x, y int) int {
    return x - y
}

func cal(s string) func(int, int) int {
    switch s {
    case "+":
        return add
    case "-":
        return sub
    default:
        return nil
    }
}

func main() {
    f := cal("+")
    r := f(1, 2)
    fmt.Printf("r: %v\n", r) // 3

    f = cal("-")
    r = f(1, 2)
    fmt.Printf("r: %v\n", r) // -1
}

匿名函数

go语言函数不能嵌套,但是可以在函数内部定义匿名函数,简单调用。

例如:

func main() {
    max := func(a, b int) int {
        if a > b {
            return a
        } else {
            return b
        }
    }

    i := max(4, 5)
    fmt.Printf("i: %v\n", i) // i: 5
}

== 闭包 ==

可以理解为定义在一个函数内部的函数。是将函数内部和函数外部连接起来的桥梁,或者说是函数和它引用环境的组合体。

闭包=函数+引用环境

demo1

外部作用域变量。

func add() func(int) int {
    var x int
    return func(y int) int {
        x += y
        return x
    }
}

func main() {
    var f = add()
    fmt.Printf("f(10): %v\n", f(10)) // f(10): 10
    fmt.Printf("f(20): %v\n", f(20)) // f(20): 30
    fmt.Printf("f(30): %v\n", f(30)) // f(30): 60

    fmt.Println("-----------------")
    f1 := add()
    fmt.Printf("f1(10): %v\n", f1(10)) // f1(10): 10
    fmt.Printf("f1(20): %v\n", f1(20)) // f1(20): 30

    fmt.Printf("f(40): %v\n", f(40)) // f(40): 100
}

f 是一个函数并且引用了外部作用域的x变量,此时f就是一个闭包,在f的生命周期内,变量x一直有效。

demo2

更改参数,构造相似功能的函数

func makeSuffixFunc(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}

func main() {
    jpgFunc := makeSuffixFunc(".jpg")
    txtFunc := makeSuffixFunc(".txt")
    fmt.Printf("jpgFunc(\"test\"): %v\n", jpgFunc("test"))   // jpgFunc("test"): test.jpg
    fmt.Printf("jpgFunc(\"1.jpg\"): %v\n", jpgFunc("1.jpg")) // jpgFunc("1.jpg"): 1.jpg
    fmt.Printf("txtFunc(\"1\"): %v\n", txtFunc("1"))         // txtFunc("1"): 1.txt
}

demo3

也可以返回多个函数,类似普通函数的返回多个参数。

package main

import "fmt"

func cal(base int) (func(int) int, func(int) int) {
    add := func(a int) int {
        base += a
        return base
    }
    sub := func(a int) int {
        base -= a
        return base
    }
    return add, sub
}

func main() {
    add, sub := cal(0)
    fmt.Printf("add(100): %v\n", add(100)) // add(100): 100
    fmt.Printf("add(20): %v\n", add(20))   // add(20): 120
    fmt.Printf("sub(10): %v\n", sub(10))   // sub(10): 110
}

递归

典型的,写一个斐波那契吧。

func f(a int) int {
    if a == 1 || a == 2 {
        return 1
    }
    return f(a-1) + f(a-2)
}

func main() {
    for i := 1; i <= 10; i++ {
        fmt.Printf("%v ", f(i)) 
        // 1 1 2 3 5 8 13 21 34 55 
    }
}

明确终止条件和进入下一层的条件。

defer

将后面跟随的语句进行延迟处理,栈的形式,先出现的后执行。

特性:

  • 用于注册延迟调用
  • 知道return前才被执行,可以用来做资源清理
  • 多个defer,先进后出执行
  • defer语句中的变量,在defer声明时就有了
func f() {
    fmt.Println("start")
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    fmt.Println("end")
}

func main() {
    f()
    // start
    // end
    // 3
    // 2
    // 1
}

golang init函数

先于main函数执行,实现包级别的一些初始化操作。

特点:

  • 先于main函数自动执行,不能被调用
  • 没有输入参数和返回值
  • 每个包可以有多个init函数
  • 包的每个源文件也可以有多个init函数
  • 同一个包init函数的执行顺序不定
  • 不同包的init函数根据依赖关系决定

初始化顺序

变量初始化 -> init() -> main()

var a int = myA()

func myA() int {
    fmt.Println("myA")
    return 3
}

func init() {
    fmt.Println("init1")
}

func init() {
    fmt.Println("init2")
}

func main() {
    fmt.Println("main...")
}

// myA
// init1
// init2
// main...

指针

取地址&,根据地址取值*

声明指针:

var 变量名 *指针类型

demo:

func main() {
    var ip *int
    fmt.Printf("ip: %v\n", ip) // ip: <nil>

    var i int = 100
    ip = &i
    fmt.Printf("ip: %v\n", ip) // ip: 0xc0000140d0

    *ip = 200
    fmt.Printf("i: %v\n", i) // i: 200
}

数组指针

go指针不能运算,要对应到数组每个元素的指针需要具体对标每个元素。

func main() {
    a := []int{1, 3, 4}
    var ptr [MAX]*int
    fmt.Println(ptr) // [<nil> <nil> <nil>]
    for i := 0; i < MAX; i++ {
        ptr[i] = &a[i]
    }
    fmt.Println(ptr) // [0xc000016150 0xc000016158 0xc000016160]
    var i int = 1
    fmt.Println(unsafe.Sizeof(i)) // 8
}

类型定义和类型别名

类型定义

type 新类型名 旧类型名

如:

type MyInt int
var i MyInt = 100
fmt.Printf("%v: %T\n", i, i) // 100: main.MyInt

类型别名

type MyInt = int
var i MyInt = 100
fmt.Printf("%v: %T\n", i, i) // 100: int

区别

类型定义是定义了一个全新的类型,但是类型别名只在代码中存在,打印二者的类型名%T也可以看出差别。

类型别名可以用原类型的方法,重定义的类型不能使用之前的方法。

结构体

go没有面向对象的概念,但是可以用结构体来实现

定义

type 变量名 struct {
    member definition;
    member definition;
    member definition;
    ...
}

例如:

type Person struct {
    id    int
    name  string
    age   int
    email string
}

func main() {
    var tom Person
    tom.age = 23
    fmt.Printf("tom: %#v\n", tom)
    kang := Person{name: "kang"}
    fmt.Printf("kang: %#v\n", kang)
    // tom: main.Person{id:0, name:"", age:23, email:""}
    // kang: main.Person{id:0, name:"kang", age:0, email:""}
}

匿名结构体

有时候只想用一次,就不用起名字,直接用一下。

func main() {
    var tom struct {
        id    int
        name  string
        age   int
        email string
    }
    tom.age = 23
    fmt.Printf("tom: %#v\n", tom)
    // tom: struct { id int; name string; age int; email string }{id:0, name:"", age:23, email:""}
}

初始化

未初始化的结构体,成员都是默认值。

kang := Person{name: "kang"}

可以声明所有,也可以声明部分,也可以不声明,在下面用:

kang.name = "kang"

单独赋值。

结构体指针

type Person struct {
    id    int
    name  string
    age   int
    email string
}

func main() {
    var p *Person
    tom := Person{
        id:    1,
        name:  "tom",
        age:   20,
        email: "tom@gmail.com",
    }
    p = &tom
    fmt.Printf("tom: %v\n", tom)   // tom: {1 tom 20 tom@gmail.com}
    fmt.Printf("p: %p\n", p)       // p: 0xc00007a480
    fmt.Printf("(*p): %v\n", (*p)) // (*p): {1 tom 20 tom@gmail.com}
}

new

func main() {
    var p = new(Person)
    p.name = "tom"
    // 本该是(*p).name = "tom", 可省略
    p.email = "df"
    p.id = 1
    p.age = 999
    fmt.Printf("p: %p\n", p)
    fmt.Printf("p: %v\n", *p)
    // p: 0xc0000c0450
    // p: {1 tom 999 df}
}

结构体作为函数参数

  • 复制,和原结构体的修改互不影响
  • 传递指针,能更改原结构体的数据
type Person struct {
    id    int
    name  string
    age   int
    email string
}

func test1(person Person) {
    person.id = 2
}

func test2(person *Person) {
    person.id = 2
}

func main() {
    p := Person{}
    p.name = "tom"
    p.email = "df"
    p.id = 1
    p.age = 999

    test1(p)

    fmt.Printf("p.id: %v\n", p.id) // p.id: 1

    test2(&p)
    fmt.Printf("p.id: %v\n", p.id) // p.id: 2
}

结构体嵌套

golang没有面向对象,也没有继承关系,但是可以用结构体嵌套来实现这种效果。

type Dog struct {
    name string
    age  int
}

type Person struct {
    name  string
    age   int
    email string
    dog   Dog
}

func main() {
    p := Person{}
    p.name = "tom"
    p.email = "df"
    p.age = 999
    p.dog = Dog{
        name: "di",
        age:  3,
    }

    fmt.Printf("p: %#v\n", p)
    // p: main.Person{name:"tom", age:999, email:"df", dog:main.Dog{name:"di", age:3}}
}

方法

可以声明一些方法,属于某个结构体。

方法就是有接收者的函数

语法格式:

type myType struct{}
func (recv myType) my_method(para) return_type {}
func (recv *myType) my_method(para) return_type {}

方法的接受者可以结构体,也可以是结构体指针,区别:

  • 值类型复制
  • 指针类型不复制
type Person struct {
    name  string
    age   int
    email string
}

func (p Person) eat() {
    fmt.Println(p.name + " eating...") // tom eating...
    p.age -= 1
}

func main() {
    p := Person{}
    p.name = "tom"
    p.email = "df"
    p.age = 999
    p.eat()
    fmt.Printf("p.age: %v\n", p.age) // p.age: 999
}

如果是指针类型?

func (p *Person) eat() {
    fmt.Println(p.name + " eating...") // tom eating...
    p.age -= 1
}

func main() {
    p := Person{}
    p.name = "tom"
    p.email = "df"
    p.age = 999
    p.eat()
    fmt.Printf("p.age: %v\n", p.age) // p.age: 998
}