golang学习笔记

使用函数的正确姿势

重要概念

函数签名: 输入与输出参数类型列表。

函数类型定义:

type Printer func(content string,str string) (n int, err error)
type Printer func(string, string) (int, error)

type Printer func(content string, id int) (n int, err error)
type Printer1 func(string, int) (int, error)
type Printer2 func(int, string) (int, error)

func main() {
    var printer Printer
    printer = echo
    printer("printer", 0)
    var printer1 Printer1
    printer1 = echo
    printer1("printer1", 1)
    //var printer2 Printer2
    //printer2 = echo//不能通过编译,类型不同
}

func echo(content string, id int) (n int, err error) {
    fmt.Println(content, id)
    return 0, nil
}

高级函数

满足以下任一条件即是高级函数:

  • 接受其他的函数作为参数传入
  • 把其他的函数作为结果返回

例子

实现一个函数,该函数操作两个整数,具体是什么操作由调用方实现.

方案一: 输入参数中输入函数实现

type operate func(x, y int) int
func calculate(x int, y int, op operate) (int, error) {
    if op == nil {
        //卫述语句
        return 0, errors.New("invalid operation")
    }
    return op(x, y), nil
}

方案二: 利用输出函数值实现

type calculateFunc func(x int, y int) (int, error)

func genCalculator(op operate) calculateFunc {
    return func(x int, y int) (int, error) {
        if op == nil {
            return 0, errors.New("invalid operation")
        }
        return op(x, y), nil
    }
}

此种高级函数实现了闭包函数, genCalculator中的匿名函数即是闭包函数. 因为该函数引用了自由变量op.

结构体及其方法

问题

  1. 空结构体占用空间大小?
  2. 空结构体用途?
  3. 可以定义多个空结构体吗?
  4. 不同空结构体对应的变量类型相同吗?
  5. 如何定义结构体的字符串表示形式?

答案

  1. 空结构体占用空间为0
  2. 空结构体用途:
    1. 节省内存,map实现set
    2. 空channel,不关注channel值
  3. 可以定义多个空结构体,各个空结构体不存在任何关系,只是恰好都不包含任何数据
  4. 不同空结构体对应的变量类型不同
  5. 定义String方法,error类型需要定义Error方法

嵌入字段

如果一个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。

  • 嵌入字段的方法集合会被无条件地合并进被嵌入类型的方法集合中。
  • 方法名称相同时, 被嵌入的方法会被屏蔽。
  • 嵌入字段是一种类型组合,不是继承。
  • 嵌入各字段之间不能出现名字相同的方法,运行时报错

两个概念

  1. 值类型 

    • var anc AnimalCategory
  2. 指针类型

    • var anc *AnimalCategory

值方法与指针方法的区别

  1. 内容修改
    • 值方法的接受者是该方法所属的那个类型之的一个副本.对该副本的修改一般不会体现在原值上, 除非这个类型本身是引用类型.
    • 而指针方法的接收者,是该方法所属的那个基本类型值的指针值的一个副本。我们在这样的方法内对该副本指向的值进行修改,却一定会体现在原值上。
  2. 值类型方法集合只包含所有接受者是值类型的方法,指针类型的方法集合包含所有接受者是值类型和指针类型的所有方法

接口类型的正确运用

知识点一

golang学习笔记_第1张图片

知识点二

给接口变量赋值时,接口变量会持有被赋予值的副本,而不是它本身.

知识点三

接口变量的值包含两个指针,一个指向动态值,一个指向类型信息。

golang学习笔记_第2张图片

知识点四

嵌入接口

同一接口内接口的方法与嵌入接口之间、各个嵌入接口之间不允许有同名的方法, 编译报错

golang学习笔记_第3张图片

知识点五

  1. 指针类型方法集合包含了某接口的所有方法, 则说该类型的指针类型实现了该接口,此时值类型不一定实现了该接口。
  2. 值类型方法集合包含了某接口的所有方法, 则说该类型的值类型实现了该接口,此时指针类型一定实现了该接口。

go语句

不要通过共享数据来通讯,恰恰相反,要以通讯的方式共享数据。

goroutine代表着并发变成模型中的用户级线程。

习题一

以下代码输出信息是?

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Printf("%p,%d\n", &i, i)
        }()
    }
    fmt.Println("hello")
    
}

习题二

如何让主routine等待其他routine?

方法一:使用time.Sleep

func method1() {
    num := 10
    for i := 0; i < num; i++ {
        go func() {
            fmt.Println("method1", i)
        }()
    }

    time.Sleep(time.Millisecond * 500)
    fmt.Println(">>>method1 is done")
}

方法二:使用通道

func method2() {
    num := 10
    sign := make(chan struct{}, num)
    for i := 0; i < num; i++ {
        go func() {
            fmt.Println("method2", i)
            sign <- struct{}{}
        }()
    }
    for j := 0; j < num; j++ {
        <-sign

    }
    fmt.Println(">>>method2 is done")
}

方法三:使用syn.WaitGroup

func method3() {
    num := 10
    wg := sync.WaitGroup{}
    for i := 0; i < num; i++ {
        wg.Add(1)
        go func() {
            defer func() {
                wg.Done()
            }()
            fmt.Println("method3", i)
        }()
    }
    wg.Wait()
    fmt.Println(">>>method3 is done")
}

习题三

如何限制routine的个数

func main() {
    var maxRoutineNum = 5
    ch := make(chan struct{}, maxRoutineNum)
    for i := 0; i < 10; i++ {
        ch <- struct{}{}
        go func(v int) {
            fmt.Println(v)
            time.Sleep(time.Second)
            <-ch
        }(i)
    }
    fmt.Println("hello")
    time.Sleep(100000)
}

习题四

怎样让我们启用的多个 goroutine 按照既定的顺序运行

主要思想: 设置全局信号,接受到对应信号时执行,反之则等待.

if、for和switch语句

例子一

func main() {
    numbers1 := []int{1, 2, 3, 4, 5, 6}
    for i := range numbers1 {
        fmt.Println(i)
    }
    fmt.Println()
}

例子二

  • range表达式只会在for语句开始执行时被求值一次
  • range求值结果会被复制,也就是被迭代的对象是range表达式结果的副本
func arrayRange() {
    numbers2 := [...]int{1, 1, 1, 1, 1, 1}
    maxIndex2 := len(numbers2) - 1
    for i, e := range numbers2 {
        if i == maxIndex2 {
            numbers2[0] += e
        } else {
            numbers2[i+1] += e
        }
    }
    fmt.Println(numbers2)
    fmt.Println()
}
func sliceRange() {
    numbers2 := []int{1, 1, 1, 1, 1, 1}
    maxIndex2 := len(numbers2) - 1
    for i, e := range numbers2 {
        if i == maxIndex2 {
            numbers2[0] += e
        } else {
            numbers2[i+1] += e
        }
    }
    fmt.Println(numbers2)
    fmt.Println()
}

switch

switch语句与case表达式之间的联系

func main() {
    value1 := [...]int8{0, 1, 2, 3, 4, 5, 6}
    switch 1 + 3 { // 这条语句无法编译通过。
    case value1[0], value1[1]:
        fmt.Println("0 or 1")
    case value1[2], value1[3]:
        fmt.Println("2 or 3")
    case value1[4], value1[5], value1[6]:
        fmt.Println("4 or 5 or 6")
    }
}

上述代码不能编译通过,因为switch中的值类型为int,而case中的类型为int8, 值类型不同所以不能比较。

func main() {
    value2 := [...]int8{0, 1, 2, 3, 4, 5, 6}
    switch value2[4] {
    case 0, 1:
        fmt.Println("0 or 1")
    case 2, 3:
        fmt.Println("2 or 3")
    case 4, 5, 6:
        fmt.Println("4 or 5 or 6")
    }
}

上述代码可以通过编译,case中的数值类型转化为int8

switch语句与case表达式之间的约束

每个case之间常量值不允许相同

func main() {
    value3 := [...]int8{0, 1, 2, 3, 4, 5, 6}
    switch value3[4] { 
    case 0, 1, 2:
        fmt.Println("0 or 1 or 2")
    case 2, 3, 4:
        fmt.Println("2 or 3 or 4")
    case 4, 5, 6:
        fmt.Println("4 or 5 or 6")
    }
}

错误处理

error类型是一个接口类型

type error interface {
    Error() string
}
package errors

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

error类型转字符串的时候调用Error()方法,而非String方法

panic、recover以及defer

从panic被引发到程序终止运行的过程

某个函数中的某行代码有意或无意地引发了一个panic。这时,初始的panic详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,也就是调用栈中的上一级。控制权限一级级沿着调用栈反方向传播至顶端.由最顶端创建的所有routine都会终止.

触发panic的方式

  • Go语言的运行时系统可能会在程序出现严重错误时自动地抛出panic
  • 在需要时也可以通过调用panic函数引发panic, 引发时建议传入error类型的错误值
func panic(v interface{})

捕获panic

困惑点:如何捕获新routine中的panic?

func main() {
    defer func() {
        if rec := recover(); rec != nil {
            fmt.Println("panic,rec,", rec)
        }
    }()
    fmt.Println("Enter function main.")
    caller1()
    fmt.Println("Exit function main.")

    time.Sleep(100000000)
    fmt.Println("sfs")
}

func caller1() {
    fmt.Println("Enter function caller1.")
    caller2()
    fmt.Println("Exit function caller1.")
}

func caller2() {
    time.Sleep(100000)
    panic("new panic")
}

defer执行顺序

defer执行顺序与定义顺序完全相反

func main() {
    defer func() {
        fmt.Println("defer 1")
    }()
    defer func() {
        fmt.Println("defer 2")
    }()
    defer func() {
        fmt.Println("defer 3")
    }()
    fmt.Println("main...")
}

你可能感兴趣的:(go)