• 【建议收藏】Go语言关键知识点总结


    容器

    数组和切片

    在Go语言中,数组和切片是两个基本的数据结构,用于存储和操作一组元素。它们有一些相似之处,但也有许多不同之处。下面我们详细介绍数组和切片的特点、用法以及它们之间的区别。

    数组

    数组是固定长度的序列,存储相同类型的元素。数组的长度在定义时就固定下来,不能改变。

    package main
    
    import "fmt"
    
    func main() {
        // 定义一个长度为5的整型数组
        var arr [5]int
        fmt.Println(arr) // 输出: [0 0 0 0 0]
    
        // 定义并初始化一个长度为5的整型数组
        arr2 := [5]int{1, 2, 3, 4, 5}
        fmt.Println(arr2) // 输出: [1 2 3 4 5]
    
        // 让编译器推断数组长度
        arr3 := [...]int{1, 2, 3}
        fmt.Println(arr3) // 输出: [1 2 3]
    }
    

    可以使用索引来访问和修改数组中的元素:

    package main
    
    import "fmt"
    
    func main() {
        arr := [3]int{1, 2, 3}
        fmt.Println(arr[0]) // 输出: 1
    
        arr[1] = 10
        fmt.Println(arr) // 输出: [1 10 3]
    }
    

    可以使用for循环来遍历数组:

    package main
    
    import "fmt"
    
    func main() {
        arr := [3]int{1, 2, 3}
        for i, v := range arr {
            fmt.Println(i, v)
        }
    }
    

    切片

    切片是动态数组,可以按需增长。切片由三个部分组成:指针、长度和容量。指针指向数组中切片的起始位置,长度是切片中的元素个数,容量是从切片起始位置到数组末尾的元素个数。

    package main
    
    import "fmt"
    
    func main() {
        // 创建一个长度和容量为3的整型切片
        slice := make([]int, 3)
        fmt.Println(slice) // 输出: [0 0 0]
    
        // 定义并初始化一个切片
        slice2 := []int{1, 2, 3, 4, 5}
        fmt.Println(slice2) // 输出: [1 2 3 4 5]
    }
    

    切片可以通过数组或另一个切片生成:

    package main
    
    import "fmt"
    
    func main() {
        arr := [5]int{1, 2, 3, 4, 5}
        slice := arr[1:4]
        fmt.Println(slice) // 输出: [2 3 4]
    }
    

    可以使用内置的append函数向切片追加元素:

    package main
    
    import "fmt"
    
    func main() {
        slice := []int{1, 2, 3}
        slice = append(slice, 4, 5)
        fmt.Println(slice) // 输出: [1 2 3 4 5]
    }
    

    其他操作和数组基本一样,下面再说下数组和切片的区别:

    1. 长度
      • 数组的长度是固定的,定义后不能改变。
      • 切片的长度是动态的,可以通过append函数增加元素。
    2. 灵活性
      • 数组在使用上较为僵化,因为长度固定,适用于元素数量已知且固定的场景。
      • 切片更加灵活,适用于需要动态添加或删除元素的场景。
    3. 性能
      • 数组的访问速度通常比切片快,因为它们是固定大小的,编译器可以进行更多的优化。
      • 切片在性能上稍逊,但由于其灵活性,使用更加广泛。

    container包

    在Go语言的标准库中,container包提供了三种常见的数据结构:(heap)、双向链表(list)和环形队列(ring)。这些数据结构为开发者提供了高效的插入、删除和访问操作。下面我们详细介绍这三个数据结构及其用法。

    heap 包实现了堆数据结构。堆是一种特殊的树状结构,可以用于实现优先队列。
    要使用 container/heap 包,必须定义一个实现 heap.Interface 接口的类型。该接口包含以下方法:

    • Len() int:返回元素数量。
    • Less(i, j int) bool:报告索引 i 处的元素是否小于索引 j 处的元素。
    • Swap(i, j int):交换索引 i 和 j 处的元素。
    • Push(x interface{}):将元素 x 添加到堆中。
    • Pop() interface{}:移除并返回堆中的最小元素。

    这些方法要求用户明确实现堆的各种操作,增加了使用的复杂度。

    package main
    
    import (
        "container/heap"
        "fmt"
    )
    
    // 定义一个实现 heap.Interface 的类型
    type IntHeap []int
    
    func (h IntHeap) Len() int           { return len(h) }
    func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
    func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
    
    func (h *IntHeap) Push(x interface{}) {
        *h = append(*h, x.(int))
    }
    
    func (h *IntHeap) Pop() interface{} {
        old := *h
        n := len(old)
        x := old[n-1]
        *h = old[0 : n-1]
        return x
    }
    
    func main() {
        h := &IntHeap{2, 1, 5}
        heap.Init(h)
        heap.Push(h, 3)
        fmt.Printf("最小元素: %d\n", (*h)[0])
        for h.Len() > 0 {
            fmt.Printf("%d ", heap.Pop(h))
        }
        // 输出: 最小元素: 1
        //      1 2 3 5
    }
    

    list

    list 包实现了双向链表(doubly linked list)。双向链表允许高效的插入和删除操作。

    package main
    
    import (
        "container/list"
        "fmt"
    )
    
    func main() {
        l := list.New()
    
        // 在链表前插入元素
        l.PushFront(1)
        l.PushFront(2)
    
        // 在链表后插入元素
        l.PushBack(3)
    
        // 遍历链表
        for e := l.Front(); e != nil; e = e.Next() {
            fmt.Println(e.Value)
        }
        // 输出:
        // 2
        // 1
        // 3
    }
    

    ring

    ring 包实现了环形队列(circular list)。环形队列是一种首尾相连的队列结构。

    package main
    
    import (
        "container/ring"
        "fmt"
    )
    
    func main() {
        // 创建一个长度为3的环
        r := ring.New(3)
    
        // 初始化环中的值
        for i := 0; i < r.Len(); i++ {
            r.Value = i
            r = r.Next()
        }
    
        // 遍历环中的元素
        r.Do(func(p interface{}) {
            fmt.Println(p.(int))
        })
        // 输出:
        // 0
        // 1
        // 2
    }
    

    Channel

    什么是Channel

    在Go语言中,channel是用于在不同的goroutine之间进行通信的机制。它可以让一个goroutine将值发送到一个通道中,另一个goroutine从通道中接收值。channel的设计使得goroutine之间的通信和同步变得简洁而高效。

    创建Channel

    创建一个channel使用make函数,指定其传递的值的类型:

    ch := make(chan int)
    

    可以创建带缓冲的channel,缓冲大小在make时指定:

    ch := make(chan int, 100)
    

    发送和接收

    发送和接收操作使用箭头符号<-:

    ch <- 1   // 发送值1到channel
    value := <-ch  // 从channel接收值并赋值给变量value
    

    关闭Channel

    channel可以被主动关闭,关闭channel使用close函数:

    close(ch)
    

    一旦一个channel被关闭,再往该channel发送值会导致panic,从已关闭的channel接收值将立即返回该类型的零值并且不会阻塞(如果通道里还存在未被接收的元素,这些元素也会正常返回,直到所有元素都被接收,才会开始返回零值)。

    其他操作

    无缓冲通道(缓冲大小为0)

    • 发送操作会阻塞直到有goroutine来接收这个值。
    • 接收操作会阻塞直到有值被发送到channel。

    缓冲通道

    • 发送操作会在缓冲区满时阻塞。
    • 接收操作会在缓冲区为空时阻塞。

    Select语句

    select语句可以用于处理多个channel操作。它会阻塞直到其中一个channel可以进行操作。select语句中的各个分支是随机选择的:

    select {
    case val := <-ch1:
        fmt.Println("Received", val)
    case ch2 <- 1:
        fmt.Println("Sent 1")
    default:
        fmt.Println("No communication")
    }
    

    示例

    基于channel,实现一个简单的生产者-消费者模型:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func producer(ch chan int) {
        //循环往通道发送5个元素,间隔1秒
        for i := 0; i < 5; i++ {
            fmt.Println("Producing", i)
            ch <- i
            time.Sleep(time.Second)
        }
        //发送完所有消息后关闭通道
        close(ch)
    }
    
    func consumer(ch chan int) {
        //可以通过range遍历通道的元素
        //因为生产者已经关闭了通道,所以遍历完所有元素后,循环会自己退出
        for val := range ch {
            fmt.Println("Consuming", val)
            time.Sleep(time.Second)
        }
    }
    
    func main() {
        ch := make(chan int, 2)
        go producer(ch)
        consumer(ch)
    }
    
    

    常见问题

    1. 避免在接收端关闭通道:通常由发送方负责关闭channel。
    2. 避免重复关闭通道:多次关闭同一个channel会导致panic。
    3. 避免从未使用的通道发送和接收:未使用的channel操作会导致死锁。比如只接收,没发送,程序会一直阻塞在接收处。

    函数

    在Go语言中,函数是一等公民(first-class citizen),这意味着函数可以像其他类型(例如整数、字符串等)一样使用和操作。这一特性使得函数的使用非常灵活和强大。具体来说,函数作为一等公民具有以下特点:

    函数可以赋值给变量

    你可以将一个函数赋值给一个变量,这样就可以通过这个变量来调用函数:

    package main
    
    import "fmt"
    
    func main() {
        add := func(a, b int) int {
            return a + b
        }
        fmt.Println(add(3, 4)) // 输出: 7
    }
    
    

    函数可以作为参数传递给另一个函数

    函数可以作为参数传递给其他函数,这使得可以实现高阶函数:

    package main
    
    import "fmt"
    
    func applyOperation(a, b int, op func(int, int) int) int {
        return op(a, b)
    }
    
    func main() {
        add := func(a, b int) int {
            return a + b
        }
        result := applyOperation(5, 3, add)
        fmt.Println(result) // 输出: 8
    }
    
    

    函数可以作为返回值从另一个函数返回

    函数可以从另一个函数返回,这使得可以动态生成函数:

    package main
    
    import "fmt"
    
    func createMultiplier(factor int) func(int) int {
        return func(x int) int {
            return x * factor
        }
    }
    
    func main() {
        double := createMultiplier(2)
        triple := createMultiplier(3)
        fmt.Println(double(4)) // 输出: 8
        fmt.Println(triple(4)) // 输出: 12
    }
    
    

    函数可以嵌套定义

    在Go语言中,可以在函数内部定义另一个函数:

    package main
    
    import "fmt"
    
    func main() {
        outer := func() {
            fmt.Println("This is the outer function.")
    
            inner := func() {
                fmt.Println("This is the inner function.")
            }
    
            inner()
        }
    
        outer()
    }
    
    

    函数可以作为匿名函数

    匿名函数是一种无需命名的函数,可以直接使用:

    package main
    
    import "fmt"
    
    func main() {
        result := func(a, b int) int {
            return a + b
        }(3, 5)
        
        fmt.Println(result) // 输出: 8
    }
    
    

    闭包(Closures)

    Go语言支持闭包,闭包是一个函数,这个函数可以捕获并记住其所在环境的变量:

    package main
    
    import "fmt"
    
    func main() {
        x := 10
    
        // 定义一个修改外部变量x的闭包
        closure := func() int {
            x += 1
            return x
        }
    
        fmt.Println(closure()) // 输出: 11
        fmt.Println(x)         // 输出: 11
    }
    
    
    package main
    
    import "fmt"
    
    func main() {
        counter := func() func() int {
            count := 0
            return func() int {
                count++
                return count
            }
        }()
        
        fmt.Println(counter()) // 输出: 1
        fmt.Println(counter()) // 输出: 2
        fmt.Println(counter()) // 输出: 3
    }
    
    

    错误处理

    Go语言中的错误处理方式不同于传统的异常处理机制。它采用了明确的、基于值的错误处理方法。每个函数可以返回一个错误值来表示是否出现了问题。

    基本错误处理

    Go语言中使用内置的error接口类型来表示错误。error接口定义如下:

    type error interface {
        Error() string
    }
    

    函数通常返回一个error类型的值来表示操作是否成功。如果没有错误,返回nil。

    package main
    
    import (
        "errors"
        "fmt"
    )
    
    // 定义一个函数,返回错误
    func divide(a, b int) (int, error) {
        if b == 0 {
            //如果有问题,通过New方法新建一个错误信息
            return 0, errors.New("division by zero")
        }
        //如果没有错误返回nil
        return a / b, nil
    }
    
    func main() {
        result, err := divide(4, 2)
        if err != nil {
            fmt.Println("Error:", err)
        } else {
            fmt.Println("Result:", result)
        }
    
        result, err = divide(4, 0)
        if err != nil {
            fmt.Println("Error:", err)
        } else {
            fmt.Println("Result:", result)
        }
    }
    

    自定义错误类型

    除了使用errors.New创建简单错误外,Go语言允许我们定义自己的错误类型,实现更丰富的错误信息。

    package main
    
    import (
        "fmt"
    )
    
    // 自定义错误类型
    type MyError struct {
        Code    int
        Message string
    }
    
    // 实现error接口的Error方法
    func (e *MyError) Error() string {
        return fmt.Sprintf("Code: %d, Message: %s", e.Code, e.Message)
    }
    
    // 定义一个函数,返回自定义错误(只要实现了Error()方法,就可以直接返回error类型)
    func doSomething(flag bool) error {
        if !flag {
            return &MyError{Code: 123, Message: "something went wrong"}
        }
        return nil
    }
    
    func main() {
        err := doSomething(false)
        if err != nil {
            fmt.Println("Error:", err)
            
            // 类型断言,获取具体的错误类型
            if myErr, ok := err.(*MyError); ok {
                fmt.Println("Custom Error Code:", myErr.Code)
            }
        }
    }
    
    

    异常处理机制

    Go语言也有类似异常的处理机制,即defer、panic和recover,但它们主要用于处理程序中不可恢复的错误。

    • defer:用于延迟执行一个函数,在函数返回前执行。如果一个函数里面有多个defer语句,写在最后面的defer最先执行。
    • panic:意料之外的错误,也可以手动调用。如果panic没有处理,程序会终止。
    • recover:恢复panic,并停止程序终止的过程。
    package main
    
    import "fmt"
    
    func main() {
        defer func() {
            //使用defer执行一个匿名函数,确保recover一定能执行
            if r := recover(); r != nil {
                //恢复panic,此处可以进行异常处理,比如打印日志
                fmt.Println("Recovered from panic:", r)
            }
        }()
    
        fmt.Println("Starting the program")
        //手动触发一个panic
        panic("Something went wrong!")
        fmt.Println("This line will not be executed")
    }
    
  • 相关阅读:
    MindSpore官方RandomChoiceWithMask算子用例报错
    SAP通过应用实例分析BOM组件中“Asm“勾选问题
    共享充电宝APP前端设计和实现
    计算机操作系统 第三章:处理机调度与死锁(4)
    进程的基本概念(操作系统)
    删除链表的倒数第n个节点
    go 中的继承与多态
    数据分析:从界定问题开始做数据分析?
    虚拟机信息巡检脚本
    通过python爬虫mechanize库爬取本机的ip地址
  • 原文地址:https://www.cnblogs.com/lbhym/p/18276068