测试golang基础

gin框架

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

详细可以参考这篇文章:

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
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如下所示:

post image

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

bucket:

post image

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

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

post image

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

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

post image

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

golang接口接收规则

  • 实体类型以值接收者实现接口的时候,不管是实体类型的值,还是实体类型值的指针,都实现了该接口。

  • 实体类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口

context包有什么用途

非常详细的说明,可以查看:

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

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

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链表,并按照先进后出的顺序执行