头图

我也挠头了

最近又有不少粉丝上岸了,其中一位分享的事情比较有意思,和你分享一下:

以后你对比Offer的时候也可以多个经验。

事情是这样的:

他在经过2个多月空窗期之后终于拿到了Offer,月薪涨幅不大,但是有绩效考核,绩效好的话年终奖还是不错的。于是这哥们就查了一下这家公司的年终奖。

不查不知道,一查吓一跳,正好查到了网上上的热点:传天X信年终奖打折,到账几百块。

他还给我发了几个热帖评论。

网友一: 天X信的年终奖,确实在闹笑话,一年累死累活到头来0年终。

网友二: 话说今年安全公司哪家发年终奖了??

网友三: 啥?年终奖200块,你嫌弃少啊,给你就不错了,这的那的~

上市公司天X信发放年终奖,人均几百元,我擦,牛不牛;员工觉得这是侮辱啊;什么努力奋进,什么归属感,什么目标责任感,钱呢?钱呢?

要不要去?

问我要不要去?去的话月薪涨幅不大,想靠绩效年终奖找补,但是又不稳~

不去的话,已经找了俩月了,已经身心疲惫了,也不确定后面还能不能找到更好的。

大家觉得这哥们是选择入职?还是继续找更好的?

我在 公众号 发起的投票如下:

来吃瓜的你又有什么想说的呢?欢迎在评论区留言讨论!

我也和这位朋友问了下,有没有整理面经,也好和大家分享一下,答案是没有,哈哈。

总结复盘面经还是灰常重要的!我建议他如果继续找,后面可得做总结复盘!

下面给大家分享一些Go后端面试必须掌握的、特别基础的知识点:

常考面试题

01 = 和 := 的区别?

=是赋值变量,:=是定义变量。

02 指针的作用

一个指针可以指向任意变量的地址,它所指向的地址在32位或64位机器上分别固定占4或8个字节。指针的作用有:

  • 获取变量的值
 import fmt
 
 func main(){
  a := 1
  p := &a//取址&
  fmt.Printf("%d\n", *p);//取值*
 }
  • 改变变量的值
// 交换函数
func swap(a, b *int) {
        *a, *b = *b, *a
}
  • 用指针替代值传入函数,比如类的接收器就是这样的。
type A struct{}

func (a *A) fun(){}

03 Go 允许多个返回值吗?

可以。通常函数除了一般返回值还会返回一个error。

04 Go 有异常类型吗?

有。Go用error类型代替try...catch语句,这样可以节省资源。同时增加代码可读性:

 _, err := funcDemo()
if err != nil {
    fmt.Println(err)
        return
}

也可以用errors.New()来定义自己的异常。errors.Error()会返回异常的字符串表示。只要实现error接口就可以定义自己的异常,

 type errorString struct {
  s string
 }
 
 func (e *errorString) Error() string {
  return e.s
 }
 
 // 多一个函数当作构造函数
 func New(text string) error {
  return &errorString{text}
 }
package main

import (
        "errors"
        "fmt"
)

func divide(a, b int) (int, error) {
        if b == 0 { //通过errors.New 可类似实现throw new Exception捕获异常
                return 0, errors.New("division by zero")
        }
        return a / b, nil
}

func main() {
        result, err := divide(10, 2)
        if err != nil {
                fmt.Println("Error:", err)
        } else {
                fmt.Println("Result:", result)
        }

        result, err = divide(10, 0)
        if err != nil {
                fmt.Println("Error:", err)
        } else {
                fmt.Println("Result:", result)
        }
}

05 什么是协程(Goroutine)

协程是用户态轻量级线程,它是线程调度的基本单位。通常在函数前加上go关键字就能实现并发。一个Goroutine会以一个很小的栈启动2KB或4KB,当遇到栈空间不足时,栈会自动伸缩, 因此可以轻易实现成千上万个goroutine同时启动。

06 ❤如何高效地拼接字符串

拼接字符串的方式有:+ fmt.Sprintf, strings.Builder, bytes.Buffer, strings.Join

  1. +

使用+操作符进行拼接时,会对字符串进 行遍历,计算并开辟一个新的空间来存储原来的两个字符串。

  1. fmt.Sprintf

由于采用了接口参数,必须要用反射获取值,因此有性能损耗。

3 strings.Builder

用WriteString()进行拼接,内部实现是指针+切片,同时String()返回拼接后的字符串,它是直接把[]byte转换为string,从而避免变量拷贝。

4 bytes.Buffer

bytes.Buffer是一个一个缓冲byte类型的缓冲器,这个缓冲器里存放着都是byte,

bytes.buffer底层也是一个[]byte切片。

5 strings.join

strings.join也是基于strings.builder来实现的,并且可以自定义分隔符,在join方法内调用了b.Grow(n)方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。

性能比较

strings.Join` ≈ `strings.Builder` > `bytes.Buffer` > `+` > `fmt.Sprintf

5种拼接方法的实例代码:

func main(){
        a := []string{"a", "b", "c"}
        //方式1:+
        ret := a[0] + a[1] + a[2]
        //方式2:fmt.Sprintf
        ret := fmt.Sprintf("%s%s%s", a[0],a[1],a[2])
        //方式3:strings.Builder
        var sb strings.Builder
        sb.WriteString(a[0])
        sb.WriteString(a[1])
        sb.WriteString(a[2])
        ret := sb.String()
        //方式4:bytes.Buffer
        buf := new(bytes.Buffer)
        buf.Write(a[0])
        buf.Write(a[1])
        buf.Write(a[2])
        ret := buf.String()
        //方式5:strings.Join
        ret := strings.Join(a,"")
}

答疑

在Go语言中,strings.Builder的性能通常比bytes.Buffer好,主要有以下几个原因:

  1. 零拷贝:strings.Builder在内部使用了可变长度的[]byte切片来存储字符串,而bytes.Buffer使用了固定长度的[]byte切片。当进行字符串拼接时,strings.Builder可以直接修改切片中的内容,而不需要进行额外的内存分配和拷贝操作,从而避免了不必要的性能开销。
  2. 预分配内存:strings.Builder在初始化时会预分配一定大小的内存空间,避免了频繁的内存分配和释放操作。这样可以减少内存分配的次数,提高性能。
  3. 字符串连接优化:strings.Builder提供了WriteString方法,可以直接将字符串追加到内部的[]byte切片中,而不需要进行类型转换和拷贝操作。这样可以减少不必要的中间步骤,提高字符串连接的效率。

需要注意的是,strings.Builderbytes.Buffer都是用于字符串拼接和缓冲的类型,选择使用哪个取决于具体的需求和场景。如果需要频繁进行字符串拼接操作,尤其是在循环中,strings.Builder通常会更高效。而如果只是简单的缓冲操作,bytes.Buffer也可以满足需求。

总结来说,strings.Builder的性能比bytes.Buffer好,主要是因为它采用了零拷贝、预分配内存和字符串连接优化等技术,避免了不必要的内存分配和拷贝操作,提高了字符串拼接的效率。

07 什么是 rune 类型

ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。

Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。

sample := "我爱GO"
runeSamp := []rune(sample)
runeSamp[0] = '你'
fmt.Println(string(runeSamp))  // "你爱GO"
fmt.Println(len(runeSamp))  // 4

08 如何判断 map 中是否包含某个 key ?

var sampleMap map[int]int
if _, ok := sampleMap[10]; ok {
        ...
} else {
        ...
}
// sampleMap[10] 返回vlaue 和 bool

09 Go 支持默认参数或可选参数吗?

不支持。但是可以利用结构体参数,或者...传入参数切片数组。

// 传入结构体参数
struct Options {
        concurrent bool
}
func pread(offset int64, len int64, o *Options) {
        ...
}
// 这个函数可以传入任意数量的整型参数
func sumN(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

10 defer 的执行顺序

defer执行顺序和调用顺序相反,类似于栈后进先出(LIFO)。

defer在return之后执行,但在函数退出之前,defer可以修改返回值。下面是一个例子:

func test() int {
        i := 0
        defer func() {
                fmt.Println("defer1")
        }()
        defer func() {
                i += 1
                fmt.Println("defer2")
        }()
        return i
}

func main() {
        fmt.Println("return", test())
}
// defer2
// defer1
// return 0

上面这个例子中,test返回值并没有修改,这是由于Go的返回机制决定的,执行Return语句后,Go会创建一个临时变量保存返回值。如果是有名返回(也就是指明返回值func test() (i int)

func test() (i int) {
        i = 0
        defer func() {
                i += 1
                fmt.Println("defer2")
        }()
        return i
}

func main() {
        fmt.Println("return", test())
}
// defer2
// return 1

这个例子中,返回值被修改了。对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改了 i,即对返回值产生了影响。

早日上岸!

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。


王中阳Go
799 声望295 粉丝