跳过正文
  1. 面试题库/

15|代码题相关

·2918 字·6 分钟
目录
Golang面试题库 - 这篇文章属于一个选集。
§ 15: 本文

1. 使用三个协程,每秒钟打印 cat dog fish(要求:顺序不能变化,协程1打印cat,协程2打印dog,协程3打印fish)
#

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(3)

    chcat := make(chan struct{})
    chdog := make(chan struct{})
    chfish := make(chan struct{})

    go printCat(&wg, chcat, chdog)
    go printDog(&wg, chdog, chfish)
    go printFish(&wg, chfish, chcat)

    // 初始启动
    chcat <- struct{}{} // 启动 cat 打印

    wg.Wait()
}

func printCat(wg *sync.WaitGroup, cat chan struct{}, dog chan struct{}) {
    defer wg.Done()
    for {
        <-cat
        fmt.Println("cat")
        time.Sleep(time.Second)
        dog <- struct{}{} // 通知 dog 打印
    }
}

func printDog(wg *sync.WaitGroup, dog chan struct{}, fish chan struct{}) {
    defer wg.Done()
    for {
        <-dog
        fmt.Println("dog")
        time.Sleep(time.Second)
        fish <- struct{}{} // 通知 fish 打印
    }
}

func printFish(wg *sync.WaitGroup, fish chan struct{}, cat chan struct{}) {
    defer wg.Done()
    for {
        <-fish
        fmt.Println("fish")
        time.Sleep(time.Second)
        cat <- struct{}{} // 通知 cat 打印
    }
}

2. 实现两个协程轮流输出 A1B2C3…Z26
#

func ChannelFunc() {
    // 思想:两个g,一个输出数字,一个输出字母,重点是如何控制两个g的打印顺序,让其可以轮流打印
    // 分别使用两个缓存为1的chan,来控制两个g的打印顺序
    strChan := make(chan int, 1)
    numChan := make(chan int, 1)

    strChan <- 0 // 先往字符chan中塞入,此时strChan再塞入会堵塞

    // 负责打印字母
    go func() {
        for i := 65; i <= 90; i++ {
            <-strChan // strChan取出,因为之前先塞入了,所以此处不会堵塞,会直接打印字母
            fmt.Printf("%v ", string(rune(i))) // 打印字母
            numChan <- i // numChan 塞入,塞入后,另一个g的numChan取出操作才能进行
        }
        return
    }()

    // 负责打印数字
    go func() {
        for i := 1; i <= 26; i++ {
            <-numChan // 一直阻塞,直到字母被打印,这样每次数字都是在字母后面被打印的
            fmt.Printf("%v ", i) /// 打印数字
            strChan <- i // strChan塞入,此处塞入后,上面协程的strChan取出操作才能进
        }
        return
    }()

    time.Sleep(1 * time.Second)
    fmt.Println()

    // 输出: A 1 B 2 C 3 D 4 E 5 F 6 G 7 H 8 I 9 J 10 K 11 L 12 M 13 N 14 O 15 P 16 Q 17 R ...
}

3. N 个 Goroutine 顺序打印数字(需要多多练习,代码不难,面对且战胜它)
#

package main

import (
    "fmt"
    "time"
)

// 多少个Goroutine (1 - n)
var n = 5

func main() {
    // 核心思路,先创建n个channel,然后创建n个goroutine,最后在main goroutine里面发号施令

    // 先创建n个channel
    slice := make([]chan int, n)
    for i := 0; i < n; i++ {
        slice[i] = make(chan int)
    }

    // 创建goroutine
    for i := 0; i < n; i++ {
        // 开启goroutine
        go Print(slice[i], i)
    }

    number := 0
    for i := 0; i < 100; i++ {
        for j := 0; j < n; j++ {
            // 发号
            slice[j] <- number
            number++
            // 休息一小会
            time.Sleep(time.Second)
        }
    }
}

func Print(ch chan int, num int) {
    for {
        fmt.Printf("goroutine %d 进行打印 %d \n", num, <-ch)
    }
}

4. 下面函数执行结果是啥(需要知道切片底层原理)
#

package main

import "fmt"

func main() {
    doAppend := func(s []int) {
        s = append(s, 1)
        printLengthAndCapacity(s)
    }

    s := make([]int, 8, 8)

    doAppend(s[:4])   // len=5 cap=8
    printLengthAndCapacity(s)  // len=8 cap=8

    doAppend(s)       // len=9 cap=16 (触发扩容)
    printLengthAndCapacity(s)  // len=8 cap=8 (原 slice 不受影响)
}

func printLengthAndCapacity(s []int) {
    fmt.Println(s)
    fmt.Printf("len=%d cap=%d \n", len(s), cap(s))
}

执行结果说明

  • s[:4] 创建了一个新的 slice header,但共享同一个底层数组,len=4, cap=8
  • s[:4] 执行 append 后,由于 cap=8 足够,直接修改了底层数组的第 5 个位置
  • 但原始 s 的 len 仍然是 8,看不到 append 的结果
  • 对完整 s 执行 append 时触发扩容,创建了新数组,原 s 不受影响

5. 下面代码的输出是啥(重在学习 GMP 模型创建一个 goroutine 背后的原理)
#

package main

import (
    "runtime"
    "sync"
)

func main() {
    // 1. 设置只有一个 P 可以工作
    runtime.GOMAXPROCS(1)

    wg := sync.WaitGroup{}

    var n = 10
    // 2. 启动n个 goroutine打印,哪个goroutine最先打印
    wg.Add(n)

    // 创建goroutine go func -> runtime.newproc
    for i := 1; i <= n; i++ {
        go func(i int) {
            // waitGroup 计数器减一,可以忽略
            defer wg.Done()
            println("I am goroutine ", i)
        }(i)
    }

    wg.Wait()
}

输出结果:由于 GOMAXPROCS(1) 只有一个 P,goroutine 的执行顺序取决于调度器的实现。通常情况下,后创建的 goroutine 可能先被执行(LIFO 调度策略),所以输出可能是逆序的,如 I am goroutine 10, 9, 8...,但这不是绝对的。

6. 有一个数组,用两个协程,一个打印所有偶数的和,一个打印所有奇数的和,要用 for channel 机制
#

package main

import (
    "fmt"
    "time"
)

func main() {
    // 测试数组
    arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // 创建两个无缓冲channel
    evenChan := make(chan int)
    oddChan := make(chan int)

    // 偶数求和协程
    go func() {
        sum := 0
        // 使用 for range 遍历channel,直到channel被关闭
        for num := range evenChan {
            sum += num
        }
        fmt.Printf("偶数和:%d\n", sum)
    }()

    // 奇数求和协程
    go func() {
        sum := 0
        // 使用 for range 遍历channel,直到channel被关闭
        for num := range oddChan {
            sum += num
        }
        fmt.Printf("奇数和:%d\n", sum)
    }()

    // 主协程负责分发数据
    fmt.Printf("原数组:%v\n", arr)
    for _, num := range arr {
        if num%2 == 0 {
            evenChan <- num // 发送偶数到偶数channel
        } else {
            oddChan <- num // 发送奇数到奇数channel
        }
    }

    // 关闭channel,通知协程没有更多数据
    close(evenChan)
    close(oddChan)

    // 等待协程完成 (简单的sleep,实际项目中建议使用WaitGroup)
    time.Sleep(100 * time.Millisecond)

    fmt.Println("计算完成")
}

7. 10 个生产者 5 个消费者,生产者总共生产 1000 个消费物料(编号 1-1000),5 个消费者并行消费
#

package main

import (
    "fmt"
    "sync"
)

func main() {
    const (
        numProducers = 10
        numConsumers = 5
        totalJobs    = 1000
    )

    jobs      := make(chan int, totalJobs)     // 生产任务通道(缓冲大小为总任务数)
    materials := make(chan int, numProducers)   // 物料传递通道(带缓冲)

    // 1. 主协程预先生成 1-1000 的任务编号
    go func() {
        for i := 1; i <= totalJobs; i++ {
            jobs <- i
        }
        close(jobs) // 任务生成完毕后关闭通道
    }()

    // 2. 启动 10 个生产者协程
    var wgProducers sync.WaitGroup
    wgProducers.Add(numProducers)

    for i := 0; i < numProducers; i++ {
        go func() {
            defer wgProducers.Done()
            for job := range jobs { // 从 jobs 通道读取任务
                materials <- job // 将物料发送到 materials 通道
            }
        }()
    }

    // 3. 等待所有生产者结束,关闭 materials 通道
    go func() {
        wgProducers.Wait()
        close(materials)
    }()

    // 4. 启动 5 个消费者协程
    var wgConsumers sync.WaitGroup
    wgConsumers.Add(numConsumers)

    for i := 0; i < numConsumers; i++ {
        go func() {
            defer wgConsumers.Done()
            for material := range materials { // 从 materials 通道读取物料
                fmt.Printf("消费者处理物料:%d\n", material)
            }
        }()
    }

    // 5. 等待所有消费者结束
    wgConsumers.Wait()
    fmt.Println("所有物料处理完毕")
}

8. 通过协程(goroutine)和通道(channel)实现多个协程执行随机数加法,最后输出其中最大值
#

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    // 设置随机数种子,确保每次运行结果不同
    rand.Seed(time.Now().UnixNano())

    const numGoroutines = 10 // 定义协程数量
    resultChan := make(chan int)

    // 启动多个协程生成随机数并计算加法
    for i := 0; i < numGoroutines; i++ {
        go func() {
            a := rand.Intn(100) // 生成0~99的随机数
            b := rand.Intn(100)
            sum := a + b
            resultChan <- sum // 将结果发送到通道
        }()
    }

    // 主协程收集结果并计算最大值
    max := 0
    for i := 0; i < numGoroutines; i++ {
        if current := <-resultChan; current > max {
            max = current
        }
    }

    fmt.Printf("最大值为:%d\n", max)
}

9. 使用两个 goroutine 交替打印 1-100 之间的奇数和偶数,输出时按照从小到大输出
#

分析

创建两个无缓冲通道,用于控制交替执行。奇数协程信号通道和偶数协程信号通道互相通知对方继续执行。

回答

package main

import (
    "fmt"
    "sync"
)

func main() {
    // 创建两个无缓冲通道,用于控制交替执行
    oddChan  := make(chan struct{}) // 奇数协程信号通道
    evenChan := make(chan struct{}) // 偶数协程信号通道

    var wg sync.WaitGroup
    wg.Add(2)

    // 奇数协程
    go func() {
        defer wg.Done()
        for i := 1; i <= 99; i += 2 {
            <-oddChan           // 等待触发信号
            fmt.Println("奇数:", i) // 打印奇数
            evenChan <- struct{}{}   // 通知偶数协程
        }
    }()

    // 偶数协程
    go func() {
        defer wg.Done()
        for i := 2; i <= 100; i += 2 {
            <-evenChan          // 等待触发信号
            fmt.Println("偶数:", i) // 打印偶数
            if i < 100 {
                oddChan <- struct{}{} // 通知奇数协程(最后一次无需通知)
            }
        }
    }()

    oddChan <- struct{}{} // 初始触发奇数协程
    wg.Wait()             // 等待两个协程完成
}

10. 创建 10 个 goroutine,id 分别是 0,1,2,3…9,每个 goroutine 只能打印最后一位是自己 id 号的数字,例如:3 号只能打印 3,13,23,33…编写一个程序,依次打印 1-10000
#

package main

import (
    "fmt"
    "sync"
)

func main() {
    const (
        maxNumber = 10000 // 最大数字
        workers   = 10    // 协程数量
    )

    var (
        mu      sync.Mutex         // 互斥锁
        cond    = sync.NewCond(&mu) // 条件变量
        current int                 // 当前处理的数字
        wg      sync.WaitGroup
    )

    wg.Add(workers)

    // 创建10个工作协程
    for id := 0; id < workers; id++ {
        go func(workerID int) {
            defer wg.Done()
            mu.Lock()
            defer mu.Unlock()

            for current < maxNumber {
                // 等待条件满足或任务完成
                for current < maxNumber && (current+1)%10 != workerID {
                    cond.Wait()
                }

                // 检查终止条件
                if current >= maxNumber {
                    return
                }

                // 处理当前数字
                current++
                fmt.Printf("%4d (worker %d)\n", current, workerID)

                // 唤醒所有协程重新检查条件
                cond.Broadcast()
            }
        }(id)
    }

    wg.Wait()
    fmt.Println("所有数字打印完成")
}
Golang面试题库 - 这篇文章属于一个选集。
§ 15: 本文