# 测试golang基础

By [空投](https://paragraph.com/@0x869875e1daa214425ed862b39abfca319bd2c8f1) · 2021-12-20

---

gin框架
=====

1、gin框架的路由是采用洋葱模型，中间件和具体接口采用同样的方式，提前放入handler，只是中间件方法中，有Next()方法，继续执行下一个，具体接口中则没有Next()方法，当然，中间件中也可以采用Abort()方法，终止程序继续往下执行。

详细可以参考这篇文章：

[https://blog.csdn.net/raoxiaoya/article/details/109444890](https://blog.csdn.net/raoxiaoya/article/details/109444890)

基础
==

struct之间可以相互比较吗
---------------

1、同一个类型的struct，且没有不可比较的类型（map,slice等），则可以进行比较。

2、一旦struct中包含不可比较的类型，或者是两个结构体，则不可进行比较，即便两个struct有相同的类型也不可以，因为地址不一样。

select
------

1、select和channel配合使用，用于等待一个或多个channel的输出。

golang在导入包的时候，”\_”/”.”二者有什么区别
-----------------------------

1、”\_”表示，导入的包仅适用里面的`init`函数，并不适用里面其他的函数。

2、”.”表示导入的包，在使用里面的函数时，可以省略包名。

slice和数组的比较
-----------

1、数组在传参的时候，是值传递，会对整个数组进行拷贝，会消耗大量的内存。

2、slice传参是传递引用，不会消耗大量内存，但同时这又是一个风险点，会改变原值。

slice的底层实现
----------

1、slice是基于数组实现的，它的底层是数组，可以理解为对底层数组的抽象，因为基于数组实现，所以它的底层的内存分配是连续的，效率非常高，还可以通过索引获取数据，可以迭代及垃圾回收优化。

切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组，设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象，其工作机制类似数组指针的一种封装。

切片对象非常小，是因为它是只有三个字段的数据结构：

*   指向底层的数组指针
    
*   切片的长度
    
*   切片的容量
    

**这三个字段就是golang操作底层数组的元数据**

![slice和array](https://storage.googleapis.com/papyrus_images/4b935aeeb48f8661acc232052e25ce90b1f37ad77563d52d154ee104bc87e6d9.png)

slice和array

slice的扩容机制是什么，有什么注意点
====================

*   首先判断，如果新申请容量大于2倍的旧容量，最终容量就是新申请的容量
    
*   否则判断，如果旧切片的长度小于1024，则最终容量就是旧容量的两倍
    
*   否则判断，如果旧切片长度大于等于1024，则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等于新申请的容量
    
*   如果最终容量计算值溢出，则最终容量就是新申请容量
    

伪代码如下：

        if cap > 2*oldCap {
            newCap = cap
        } else if cap < 1024 {
            newCap = 2*oldCap
        } else {
            newCap = oldCap
            while (newCap < cap && !overflow) {
                newcap += newcap/4
            }
            if overflow {
                newCap = cap
            }
        }
    

扩容前后的slice是否相同？
===============

分两种情况。

**情况一：**

**情况二：**

> 原来数组的容量已经达到了最大值，再想扩容， Go 默认会先开一片内存区域，把原来的值拷贝过来，然后再执行 append() 操作。这种情况丝毫不影响原数组。

**要复制一个Slice，最好使用Copy函数。**

golang的参数传递、引用类型
================

Go语言中**所有的传参都是值传递**（传值），都是一个副本，一个拷贝。因为拷贝的内容有时候是非引用类型（int、string、struct等这些），这样就在函数中就无法修改原内容数据；有的是**引用类型**（**指针、map、slice、chan**等这些），这样就可以修改原内容数据。

Golang的引用类型包括 slice、map 和 channel。它们有复杂的内部结构，除了申请内存外，还需要初始化相关属性。内置函数 new 计算类型大小，为其分配零值内存，返回指针。而 make 会被编译器翻译成具体的创建函数，由其分配内存和初始化成员结构，返回对象而非指针。（**这也是new和make的区别**）

golang map底层实现
==============

Golang中map的底层实现是一个散列表，因此实现map的过程实际上就是实现散表的过程。在这个散列表中，主要出现的结构体有两个，一个叫hmap(a header for a go map)，一个叫bmap(a bucket for a Go map，通常叫其bucket)。hmap如下所示：

![](https://storage.googleapis.com/papyrus_images/ca1d656b0dde256f110702f67d1de6cce0d64ba699958abeced6b4451bd7ed1f.png)

图中有很多字段，但是便于理解map的架构，你只需要关心的只有一个，就是标红的字段：buckets数组。Golang的map中用于存储的结构是bucket数组。而bucket(即bmap)的结构是怎样的呢？

bucket：

![](https://storage.googleapis.com/papyrus_images/a84c6ac70ae337f1ea16a07396775f5bd1c2c90dc4ac755866b4110932304995.png)

相比于hmap，bucket的结构显得简单一些，标红的字段依然是“核心”，我们使用的map中的key和value就存储在这里。“高位哈希值”数组记录的是当前bucket中key相关的“索引”，稍后会详细叙述。还有一个字段是一个指向扩容后的bucket的指针，使得bucket会形成一个链表结构。

整体的结构应该是这样的：

![](https://storage.googleapis.com/papyrus_images/33c48e28c880077ff1ff1ed84cfef9c37c879c8d7e1fec987656d8b83c9d6116.png)

Golang把求得的哈希值按照用途一分为二：高位和低位。低位用于寻找当前key属于hmap中的哪个bucket，而高位用于寻找bucket中的哪个key。

需要特别指出的一点是：map中的key/value值都是存到同一个数组中的。这样做的好处是：在key和value的长度不同的时候，可以消除padding带来的空间浪费。

![](https://storage.googleapis.com/papyrus_images/d502ae1a80ea05fde2cd39c83f22e1b2b821d44ba22bd973110592011f0215f6.png)

Map的扩容： 当Go的map长度增长到大于加载因子所需的map长度时，Go语言就会将产生一个新的bucket数组，然后把旧的bucket数组移到一个属性字段oldbucket中。 注意：并不是立刻把旧的数组中的元素转义到新的bucket当中，而是，只有当访问到具体的某个bucket的时候，会把bucket中的数据转移到新的bucket中。

golang接口接收规则
============

*   实体类型以值接收者实现接口的时候，不管是实体类型的值，还是实体类型值的指针，都实现了该接口。
    
*   实体类型以指针接收者实现接口的时候，只有指向这个类型的指针才被认为实现了该接口
    

context包有什么用途
=============

非常详细的说明，可以查看：

[https://studygolang.com/articles/9624](https://studygolang.com/articles/9624)

*   程序超时
    
    我们起一个本地的 http serice，名字叫"lazy"，这个 http server 会随机的发出一些慢请求，要等6秒以上才返回，我们使用这个程序来模拟我们的被调用方 hang 住的情况，代码如下：
    
        package main
        
        import (
            "net/http"
            "math/rand"
            "fmt"
            "time"
        )
        
        
        func lazyHandler(w http.ResponseWriter, req *http.Request) {
            ranNum := rand.Intn(2)
            if ranNum == 0 {
                time.Sleep(6 * time.Second)
                fmt.Fprintf(w, "slow response, %d\n", ranNum)
                fmt.Printf("slow response, %d\n", ranNum)
                return
            }
            fmt.Fprintf(w, "quick response, %d\n", ranNum)
            fmt.Printf("quick response, %d\n", ranNum)
            return
        }
        
        func main() {
            http.HandleFunc("/", lazyHandler)
            http.ListenAndServe(":9200", nil)
        }
        
    
    然后我们写一个主动调用的 http service，他会调用我们刚才写的"lazy"，我们使用 context，来解决超过2秒的慢请求问题，如下代码：
    
        package main
        
        import (
            "context"
            "net/http"
            "fmt"
            "sync"
            "time"
            "io/ioutil"
        )
        
        var (
            wg sync.WaitGroup
        )
        
        type ResPack struct {
            r *http.Response
            err error
        }
        
        func work(ctx context.Context) {
            tr := &http.Transport{}
            client := &http.Client{Transport: tr}
            defer wg.Done()
            c := make(chan ResPack, 1)
        
            req, _ := http.NewRequest("GET", "http://localhost:9200", nil)
            go func() {
                resp, err := client.Do(req)
                pack := ResPack{r: resp, err: err}
                c <- pack
            }()
        
            select {
            case <-ctx.Done():
                tr.CancelRequest(req)
                <-c
                fmt.Println("Timeout!")
            case res:= <-c:
                if res.err != nil {
                    fmt.Println(res.err)
                    return
                }
                defer res.r.Body.Close()
                out, _ := ioutil.ReadAll(res.r.Body)
                fmt.Printf("Server Response: %s", out)
            }
            return
        }
        
        
        func main() {
            ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
            defer cancel()
            wg.Add(1)
            go work(ctx)
            wg.Wait()
            fmt.Println("Finished")
        }
        
        在 main 函数中，我们定义了一个超时时间为2秒的 context，传给真正做事的work()，work接收到这个 ctx 的时候，需要等待 ctx.Done() 返回，因为 channel 关闭的时候，ctx.Done() 会受到空值，当 ctx.Done()返回时，就意味着 context 已经超时结束，要做一些扫尾工作然后 return 即可。
        
    
*   生成Request ID中间件
    
    在 Golang1.7 中，`"net/http"`原生支持将Context嵌入到 `*http.Request`中，并且提供了`http.Request.Conext()` 和 `http.Request.WithContext()`这两个函数来新建一个 context 和 将 context 加入到一个`http.Request`实例中。下面的程序演示了一下利用`WithValue()`创建一个可以储存 K-V 的 context，然后写一个中间件来自动获取 http头部的 "X-Rquest-ID"值，加入到 context 中，使业务函数可以直接取到该值，代码如下：
    
        package main
        
        import (
            "net/http"
            "context"
            "fmt"
        )
        
        const requestIDKey = "rid"
        
        func newContextWithRequestID(ctx context.Context, req *http.Request) context.Context {
            reqID := req.Header.Get("X-Request-ID")
            if reqID == "" {
                reqID = "0"
            }
            return context.WithValue(ctx, requestIDKey, reqID)
        }
        
        func requestIDFromContext(ctx context.Context) string {
            return ctx.Value(requestIDKey).(string)
        }
        
        func middleWare(next http.Handler) http.Handler {
            return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
                ctx := newContextWithRequestID(req.Context(), req)
                next.ServeHTTP(w, req.WithContext(ctx))
            })
        }
        
        func h(w http.ResponseWriter, req *http.Request) {
            reqID := requestIDFromContext(req.Context())
            fmt.Fprintln(w, "Request ID: ", reqID)
            return
        }
        
        func main() {
            http.Handle("/", middleWare(http.HandlerFunc(h)))
            http.ListenAndServe(":9201", nil)
        }
        
    

golang为什么高效
-----------

1、goroutine是golang并行设计的核心。

2、goroutine说到底就是协程，但是它比线程更小，几十个协程可能底层对应的就是五六个线程，golang内部帮你实现了这些协程之间的内存共享。

3、执行协程，只需要极少的栈内存，大概是4~5kb，也正因为这样，在一台机器上可以开启成千上万个协程，只要内存足够即可。

4、golang既支持通过共享内存进行通讯，也支持通过消息传递的方式进行通讯，后者是CSP的一个变种，效率更高，也是golang推荐的方式。

**总结为：用户空间 避免了内核态和用户态的切换导致的成本、可以由语言和框架层进行调度、更小的栈空间允许创建大量的实例。**

关于golang的CSP模型说明，可以查看这篇文章：

[https://www.jianshu.com/p/36e246c6153d](https://www.jianshu.com/p/36e246c6153d)

特别需要注意的代码执行题目
=============

1、以下程序输出什么

    package main
    
    import (
        "fmt"
    )
    
    func calc(index string, a, b int) int {
        ret := a + b
        fmt.Println(index, a, b, ret)
        return ret
    }
    
    func main() {
        a := 1
        b := 2
        defer calc("1", a, calc("10", a, b))
        a = 0
        defer calc("2", a, calc("20", a, b))
        b = 1
        //time.Sleep(1000 * time.Second)
    }
    
    output:
    10 1 2 3
    20 0 2 2
    2 0 2 2
    1 1 3 4
    

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        s := make([]int, 5)
        s = append(s, 1, 2, 3)
        fmt.Println(s)
    }
    output:
    [0 0 0 0 0 1 2 3]
    

    package main
    
    import (
        "fmt"
    )
    
    type People interface {
        Speak(string) string
    }
    
    type Stduent struct{}
    
    func (stu *Stduent) Speak(think string) (talk string) {
        if think == "bitch" {
            talk = "You are a good boy"
        } else {
            talk = "hi"
        }
        return
    }
    
    func main() {
        var peo People = Stduent{}
        think := "bitch"
        fmt.Println(peo.Speak(think))
    }
    
    //编译不过去，需要修改一行代码：
    var peo People = Stduent{} 修改为 var peo People = &Stduent{}
    

    package main
    
    import (
        "fmt"
    )
    
    type People interface {
        Show()
    }
    
    type Student struct{}
    
    func (stu *Student) Show() {
    
    }
    
    func live() People {
        var stu *Student
        return stu
    }
    
    func main() {
        if live() == nil {
            fmt.Println("AAAAAAA")
        } else {
            fmt.Println("BBBBBBB")
        }
    }
    
    output:
    BBBBBBB
    原因：指针类型的地址和nil不一致，不相等
    

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        defer_call()
    }
    
    func defer_call() {
        defer func() { fmt.Println("打印前") }()
        defer func() { fmt.Println("打印中") }()
        defer func() { fmt.Println("打印后") }()
    
        panic("触发异常")
    }
    output:
    打印后
    打印中
    打印前
    panic: 触发异常
    
    原因：遇到panic，遍历本协程的defer链表，并按照先进后出的顺序执行

---

*Originally published on [空投](https://paragraph.com/@0x869875e1daa214425ed862b39abfca319bd2c8f1/golang)*
