我也挠头了
最近又有不少粉丝上岸了,其中一位分享的事情比较有意思,和你分享一下:
以后你对比Offer的时候也可以多个经验。
事情是这样的:
他在经过2个多月空窗期之后终于拿到了Offer,月薪涨幅不大,但是有绩效考核,绩效好的话年终奖还是不错的。于是这哥们就查了一下这家公司的年终奖。
不查不知道,一查吓一跳,正好查到了网上上的热点:传天X信年终奖打折,到账几百块。
他还给我发了几个热帖评论。
网友一: 天X信的年终奖,确实在闹笑话,一年累死累活到头来0年终。
网友二: 话说今年安全公司哪家发年终奖了??
网友三: 啥?年终奖200块,你嫌弃少啊,给你就不错了,这的那的~
上市公司天X信发放年终奖,人均几百元,我擦,牛不牛;员工觉得这是侮辱啊;什么努力奋进,什么归属感,什么目标责任感,钱呢?钱呢?
要不要去?
问我要不要去?去的话月薪涨幅不大,想靠绩效年终奖找补,但是又不稳~
不去的话,已经找了俩月了,已经身心疲惫了,也不确定后面还能不能找到更好的。
大家觉得这哥们是选择入职?还是继续找更好的?
我在 公众号 发起的投票如下:
来吃瓜的你又有什么想说的呢?欢迎在评论区留言讨论!
我也和这位朋友问了下,有没有整理面经,也好和大家分享一下,答案是没有,哈哈。
总结复盘面经还是灰常重要的!我建议他如果继续找,后面可得做总结复盘!
下面给大家分享一些Go后端面试必须掌握的、特别基础的知识点:
常考面试题
01 = 和 := 的区别?
=是赋值变量,:=是定义变量。
02 指针的作用
一个指针可以指向任意变量的地址,它所指向的地址在32位或64位机器上分别固定占4或8个字节。指针的作用有:
- 获取变量的值
import fmtfunc main(){a := 1p := &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 mainimport ("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
+
使用+操作符进行拼接时,会对字符串进 行遍历,计算并开辟一个新的空间来存储原来的两个字符串。
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.Sprintfret := fmt.Sprintf("%s%s%s", a[0],a[1],a[2])//方式3:strings.Buildervar sb strings.Buildersb.WriteString(a[0])sb.WriteString(a[1])sb.WriteString(a[2])ret := sb.String()//方式4:bytes.Bufferbuf := new(bytes.Buffer)buf.Write(a[0])buf.Write(a[1])buf.Write(a[2])ret := buf.String()//方式5:strings.Joinret := strings.Join(a,"")
}
答疑
在Go语言中,strings.Builder
的性能通常比bytes.Buffer
好,主要有以下几个原因:
- 零拷贝:
strings.Builder
在内部使用了可变长度的[]byte
切片来存储字符串,而bytes.Buffer
使用了固定长度的[]byte
切片。当进行字符串拼接时,strings.Builder
可以直接修改切片中的内容,而不需要进行额外的内存分配和拷贝操作,从而避免了不必要的性能开销。 - 预分配内存:
strings.Builder
在初始化时会预分配一定大小的内存空间,避免了频繁的内存分配和释放操作。这样可以减少内存分配的次数,提高性能。 - 字符串连接优化:
strings.Builder
提供了WriteString
方法,可以直接将字符串追加到内部的[]byte
切片中,而不需要进行类型转换和拷贝操作。这样可以减少不必要的中间步骤,提高字符串连接的效率。
需要注意的是,strings.Builder
和bytes.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 := 0for _, num := range nums {total += num}return total
}
10 defer 的执行顺序
defer执行顺序和调用顺序相反,类似于栈后进先出(LIFO)。
defer在return之后执行,但在函数退出之前,defer可以修改返回值。下面是一个例子:
func test() int {i := 0defer func() {fmt.Println("defer1")}()defer func() {i += 1fmt.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 = 0defer func() {i += 1fmt.Println("defer2")}()return i
}func main() {fmt.Println("return", test())
}
// defer2
// return 1
这个例子中,返回值被修改了。对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改了 i,即对返回值产生了影响。
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。