go语言基础

1、写出下面代码的输出结果,并给出原因和修正。

1
2
3
4
5
6
7
8
9
10
func main() {
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
}()
}
time.Sleep(2 * time.Second)
fmt.Println("end")
}

解答:v 的地址是不会变化的,但是 goroutine 的运行又是不可预期的,当循环执行完后goroutine才开始执行,此时v的值为c, 然后3次goroutine都会打印c。

1
2
3
4
c
c
c
end

如果要输出 a b c得这样修正, goroutine 的时候将参数传入到匿名函数中,即使是这样,abc的顺序也是不可预期的,打印多次都是乱序。

1
2
3
4
5
6
7
8
9
10
func main() {
values := []string{"a", "b", "c"}
for _, v := range values {
go func(v string) {
fmt.Println(v)
}(v)
}
time.Sleep(2 * time.Second)
fmt.Println("end")
}

2、下面代码有问题吗?应该如何修复?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var stringList []string

func add(s string) {
stringList = append(stringList, s)
}
func main() {
for i := 0; i < 5; i++ {
go func(i int) {
add(strconv.Itoa(i))
}(i)
}
time.Sleep(1 * time.Second)
fmt.Println(stringList)
}

slice非线程安全的,goroutine的运行不可预测,所以stringList里面是乱序且元素不一定是0~4的全集合
非线程安全的可以加锁,但是元素也不是有序的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var stringList []string
var mu sync.Mutex

func add(s string) {
mu.Lock()
stringList = append(stringList, s)
mu.Unlock()
}
func main() {
for i := 0; i < 5; i++ {
go func(i int) {
add(strconv.Itoa(i))
}(i)
}
time.Sleep(1 * time.Second)
fmt.Println(stringList)
}

3、下面这段代码有什么输出?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type People struct{}

func (p *People) ShowA() {
fmt.Println("showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("showB")
}
type Teacher struct {
People
}
func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}
func main() {
t := Teacher{}
t.ShowA()
t.ShowB()
}

showA
showB
teacher showB

4、下面这段代码会输出什么,如何修复?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func test0() {
var jsonBlob = []byte(`{"name": "Platypus", "order":"Monotremata", "id": 1}`)
fmt.Println("jsonBlob: ", string(jsonBlob))
type Animal struct {
id int32 `json:"id,omitempty"`
name string `json:"name,omitempty"`
order string `json:"order,omitempty"`
}
var animals Animal
err := json.Unmarshal(jsonBlob, &animals)
if err != nil {
fmt.Println("error:", err)
}
fmt.Printf("animals: %+v\n", animals)
}

解答:
jsonBlob: {“name”: “Platypus”, “order”:”Monotremata”, “id”: 1}
animals: {id:0 name: order:}
反序列号的时候对应的结构体需要是导出的,将结构体改为导出字段即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func test0() {
var jsonBlob = []byte(`{"name": "Platypus", "order":"Monotremata", "id": 1}`)
fmt.Println("jsonBlob: ", string(jsonBlob))
type Animal struct {
ID int32 `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Order string `json:"order,omitempty"`
}
var animals Animal
err := json.Unmarshal(jsonBlob, &animals)
if err != nil {
fmt.Println("error:", err)
}
fmt.Printf("animals: %+v\n", animals)
}

5、输出的结果为?为什么?
func Testlface() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
var (
data *int
eface interface{}
)
eface = data
fmt.Println(data == nil)
fmt.Println(eface == nil)
}
[0 0 0 0 0 1 2 3]
true
false
eface 不是 nil 的原因是它是一个接口类型,被赋值为 *int 类型的指针。即使指针 data 没有被初始化并且是 nil,接口 eface 仍然持有一个 *int 类型的值,因此不是 nil。当一个接口被赋值为特定类型的值时,它包含两个组件 - 值的类型和值的指针。在这种情况下,eface 持有一个 *int 类型的值,这意味着它包含一个指向整数值的指针(即使该指针当前为 nil

6、下面代码是否有错?为什么?

1
2
3
4
5
6
7
func Testlface2() {
data := []int{1, 2, 3}
printAny(data)
}
func printAny(any []interface{}) {
fmt.Println(any)
}

不能将slice转为接口类型切片,应该直接转为接口类型就可以了,修改为

1
2
3
4
5
6
7
func Testlface2() {
data := []int{1, 2, 3}
printAny(data)
}
func printAny(any interface{}) {
fmt.Println(any)
}

7、下面的代码会有什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
abc := make(chan int, 1000)
for i := 0; i < 10; i++ {
abc <- i
}
go func() {
for {
a := <-abc
fmt.Println("a: ", a)
}
}()
close(abc)
fmt.Println(" close")
time.Sleep(time.Second * 100)
}

读取关闭的chan会返回空值,所以输出0~9后会不断的输出0

8、下面代码会怎么样?为什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
var s []int
s[0] = 0
}()
wg.Wait()
}

这段代码会导致运行时错误,因为在 go 协程中创建的切片 s 没有被初始化,所以它的长度为 0,无法访问或设置其索引为 0 的元素。这会导致 panic,并且程序会崩溃。
会输出 panic: runtime error: index out of range [0] with length 0

9、假设有100个[]int切片,现在要计算这些切片元素的总和,并且要求最大只有10个goroutine同时在计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func main() {
// 生成100个[]int切片
var slices [][]int
for i := 0; i < 100; i++ {
s := make([]int, 1000)
for j := range s {
s[j] = j
}
slices = append(slices, s)
}

// 使用errgroup包和带缓冲的通道限制最大并发数为10
var eg errgroup.Group
total := 0
limit := make(chan struct{}, 10)

for _, s := range slices {
s := s // 必须使用局部变量
eg.Go(func() error {
sum := 0
for _, v := range s {
sum += v
}

// 限制最大并发数为10
limit <- struct{}{}
defer func() { <-limit }()

// 将计算结果累加到总和中
atomic.AddInt(&total, sum)
return nil
})
}

// 等待所有goroutine完成并检查是否有错误
if err := eg.Wait(); err != nil {
fmt.Println("Error:", err)
return
}

fmt.Println("Total sum:", total)
}

10、给出LRU-Cache的伪代码(数据结构及大致思路)。cache支持协程安全,支持get、put操作,每次get、put更新key的存活时间。
以下是一个简单的LRU-Cache的伪代码,该cache支持协程安全,支持get、put操作,每次get、put更新key的存活时间。

数据结构:

  1. 双向链表:用于维护key的访问顺序,最近访问的key放在链表头部,最少访问的key放在链表尾部。
  2. 哈希表:用于快速查找key对应的节点。

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
type Node struct {
key int
value int
prev *Node
next *Node
time time.Time // 存活时间
}

type LRUCache struct {
capacity int
size int
head *Node
tail *Node
cache map[int]*Node // key -> node
mutex sync.Mutex // 用于协程安全
}

func NewLRUCache(capacity int) *LRUCache {
return &LRUCache{
capacity: capacity,
size: 0,
head: nil,
tail: nil,
cache: make(map[int]*Node),
}
}

func (c *LRUCache) Get(key int) (int, bool) {
c.mutex.Lock()
defer c.mutex.Unlock()

if node, ok := c.cache[key]; ok {
// 更新存活时间
node.time = time.Now()
// 将节点移动到链表头部
c.moveToHead(node)
return node.value, true
}
return 0, false
}

func (c *LRUCache) Put(key int, value int) {
c.mutex.Lock()
defer c.mutex.Unlock()

if node, ok := c.cache[key]; ok {
// 更新存活时间和值
node.time = time.Now()
node.value = value
// 将节点移动到链表头部
c.moveToHead(node)
} else {
// 创建新节点并加入到链表头部
node := &Node{
key: key,
value: value,
time: time.Now(),
}
c.cache[key] = node
c.addToHead(node)
c.size++

// 如果超过容量,则删除链表尾部的节点
if c.size > c.capacity {
removed := c.removeTail()
delete(c.cache, removed.key)
c.size--
}
}
}

func (c *LRUCache) addToHead(node *Node) {
if c.head == nil {
c.head = node
c.tail = node
} else {
node.next = c.head
c.head.prev = node
c.head = node
}
}

func (c *LRUCache) removeNode(node *Node) {
if node == c.head {
c.head = node.next
} else if node == c.tail {
c.tail = node.prev
} else {
node.prev.next = node.next
node.next.prev = node.prev
}
node.prev = nil
node.next = nil
}

func (c *LRUCache) moveToHead(node *Node) {
c.removeNode(node)
c.addToHead(node)
}

func (c *LRUCache) removeTail() *Node {
node := c.tail
c.removeNode(node)
return node
}

在这个伪代码中,我们首先定义了一个 Node 结构体,用于表示链表中的节点。每个节点包含一个 key、一个 value、一个 prev 指针、一个 next 指针和一个 time 时间戳,用于表示该节点的存活时间。

然后,我们定义了一个 LRUCache 结构体,用于表示LRU-Cache。每个 LRUCache 包含一个容量 capacity、一个大小 size、一个链表头部 head、一个链表尾部 tail、一个哈希表 cache 和一个互斥锁 mutex,用于协程安全。

LRUCache 中,我们定义了 NewLRUCacheGetPutaddToHeadremoveNodemoveToHeadremoveTail 等方法。其中,NewLRUCache方法用于创建一个新的 LRUCache` 实例,需要传入一个容量参数。

Get 方法用于获取指定key对应的value。首先,我们使用互斥锁进行协程安全保护。然后,我们从哈希表中查找key对应的节点,如果找到了节点,则更新节点的存活时间,并将节点移动到链表头部,表示该节点是最近访问的节点。最后,返回节点的value和true。如果没有找到节点,则返回0和false。

Put 方法用于插入一个新的key-value对或更新一个已有的key的value。首先,我们使用互斥锁进行协程安全保护。然后,我们从哈希表中查找key对应的节点,如果找到了节点,则更新节点的存活时间和value,并将节点移动到链表头部,表示该节点是最近访问的节点。如果没有找到节点,则创建一个新的节点,并将节点加入到链表头部。如果插入新节点后超过了容量,则删除链表尾部的节点。最后,更新哈希表和大小。

addToHead 方法用于将指定节点插入到链表头部。

removeNode 方法用于从链表中删除指定节点。

moveToHead 方法用于将指定节点移动到链表头部。

removeTail 方法用于删除链表尾部的节点,并返回被删除的节点。

在这个伪代码中,我们使用了一个双向链表和一个哈希表来实现LRU-Cache。在链表中,最近访问的节点放在链表头部,最少访问的节点放在链表尾部。在哈希表中,key对应的value是链表中的节点。使用双向链表和哈希表可以实现快速的访问和插入/删除操作,时间复杂度为O(1)。同时,使用互斥锁可以保证并发访问的安全性。