常考的点包括:切片,通道,异常处理,Goroutine,GMP模型,字符串高效拼接,指针,反射,接口,sync,go test和相关工具链

面试题1

来源:geektutu
解答:迹寒

= 和 := 的区别?

=是赋值变量,:=是定义变量(声明变量并赋值)。

指针的作用

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

  • 获取变量的值
  • 改变变量的值
  • 用指针替代值传入函数

Go 允许多个返回值吗?

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

Go 有异常类型吗?

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

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

1
2
3
4
5
6
7
8
9
10
11
12
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

// 多一个函数当作构造函数
func New(text string) error {
return &errorString{text}
}

什么是协程(Goroutine)

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

如何高效地拼接字符串

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

  1. “+”
    使用+操作符进行拼接时,会对字符串进行遍历,计算并开辟一个新的空间来存储原来的两个字符串。
  2. fmt.Sprint
    由于采用了接口参数,必须要用反射获取值,因此有性能损耗。
  3. strings.Builder
    用WriteString()进行拼接,内部实现是指针+切片,同时String()返回拼接后的字符串,它是直接把[]byte转换为string,从而避免变量拷贝。

strings.builder 的实现原理很简单,结构如下:

1
2
3
4
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte // 1
}

addr字段主要是做copycheck,buf字段是一个byte类型的切片,这个就是用来存放字符串内容的,提供的writeString()方法就是像切片buf中追加数据:

1
2
3
4
5
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}

提供的String方法就是将[]byte转换为string类型,这里为了避免内存拷贝的问题,使用了强制转换来避免内存拷贝

1
2
3
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
  1. bytes.Buffer

bytes.Buffer是一个一个缓冲byte类型的缓冲器,这个缓冲器里存放着都是byte。使用方式如下:

bytes.buffer底层也是一个[]byte切片,结构体如下:

1
2
3
4
5
type Buffer struct {
buf []byte // contents are the bytes buf[off : len(buf)]
off int // read at &buf[off], write at &buf[len(buf)]
lastRead readOp // last read operation, so that Unread* can work correctly.
}

因为bytes.Buffer可以持续向Buffer尾部写入数据,从Buffer头部读取数据,所以off字段用来记录读取位置,再利用切片的cap特性来知道写入位置,这个不是本次的重点,重点看一下WriteString方法是如何拼接字符串的:

1
2
3
4
5
6
7
8
func (b *Buffer) WriteString(s string) (n int, err error) {
b.lastRead = opInvalid
m, ok := b.tryGrowByReslice(len(s))
if !ok {
m = b.grow(len(s))
}
return copy(b.buf[m:], s), nil
}

切片在创建时并不会申请内存块,只有在往里写数据时才会申请,首次申请的大小即为写入数据的大小。如果写入的数据小于64字节,则按64字节申请。采用动态扩展slice的机制,字符串追加采用copy的方式将追加的部分拷贝到尾部,copy是内置的拷贝函数,可以减少内存分配。

但是在将[]byte转换为string类型依旧使用了标准类型,所以会发生内存分配:

1
2
3
4
5
6
7
func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}
  1. strings.Join

strings.join也是基于strings.builder来实现的,并且可以自定义分隔符,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func Join(elems []string, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return elems[0]
}
n := len(sep) * (len(elems) - 1)
for i := 0; i < len(elems); i++ {
n += len(elems[i])
}

var b Builder
b.Grow(n)
b.WriteString(elems[0])
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(s)
}
return b.String()
}

唯一不同在于在join方法内调用了b.Grow(n)方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。

总结

  • strings.Join ≈ strings.Builder > bytes.Buffer > “+” > fmt.Sprintf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main(){
a := []string{"a", "b", "c"}
//方式1:
ret := a[0] + a[1] + a[2]
//方式2:
ret := fmt.Sprintf("%s%s%s", a[0],a[1],a[2])
//方式3:
var sb strings.Builder
sb.WriteString(a[0])
sb.WriteString(a[1])
sb.WriteString(a[2])
ret := sb.String()
//方式4:
buf := new(bytes.Buffer)
buf.Write(a[0])
buf.Write(a[1])
buf.Write(a[2])
ret := buf.String()
//方式5:
ret := strings.Join(a,"")
}

什么是rune类型

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

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

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

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

1
2
3
4
5
6
var sample map[string]int
if _,ok := sample["key"];ok{
// 含有
} else {
//
}

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

不支持。
可以通过结构体参数,或利用 … 传入参数切片

defer 的执行顺序

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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会创建一个临时变量保存返回值。如果是有名返回(也就是指明返回值functest()(i int))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,即对返回值产生了影响。

如何交换 2 个变量的值?

1
2
a, b = b, a
*a, *b = *b, *a

Go 语言tag的用处

tag可以为结构体成员提供属性。常见的

  1. json序列化或反序列化时字段的名称
  2. db: sqlx模块中对应的数据库字段名
  3. form: gin框架中对应的前端的数据字段名
  4. binding: 搭配 form 使用, 默认如果没查找到结构体中的某个字段则不报错值为空, binding为 required 代表没找到返回错误给前端

如何获取一个结构体的所有tag?

利用反射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import reflect
type Author struct {
Name int `json:Name`
Publications []string `json:Publication,omitempty`
}

func main() {
t := reflect.TypeOf(Author{})
for i := 0; i < t.NumField(); i++ {
name := t.Field(i).Name
s, _ := t.FieldByName(name)
fmt.Println(name, s.Tag)
}
}

上述例子中,reflect.TypeOf方法获取对象的类型,之后NumField()获取结构体成员的数量。 通过Field(i)获取第i个成员的名字。 再通过其Tag 方法获得标签。

如何判断 2 个字符串切片(slice) 是相等的?

  1. reflect.DeepEqual
    官方提供的方法,利用了反射,耗时较大
    不知道切片类型时推荐此方法
  2. 循环切片
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    	// 需要明确知道切片的类型,例如:
    func ForEqual(x, y []byte) bool {
    if len(x) != len(y) {
    return false
    }

    if (x == nil) != (y == nil) {
    return false
    }

    for i, v := range x {
    if v != y[i] {
    return false
    }
    }

    return true
    }
    当明确知道需要比较的切片类型时,建议自己写比较方法。

结构体打印时,%v 和 %+v 的区别

  • %v输出结构体各成员的值;
  • %+v输出结构体各成员的名称和值;
  • %#v输出结构体名称和结构体各成员的名称和值

Go 语言中如何表示枚举值(enums)?

在常量中用iota可以表示枚举。iota从0开始。

1
2
3
4
5
6
7
8
9
const (
B = 1 << (10 * iota)
KiB
MiB
GiB
TiB
PiB
EiB
)

空 struct{} 的用途

  • 用map模拟一个set,那么就要把值置为struct{},struct{}本身不占任何空间,可以避免任何多余的内存分配。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    type Set map[string]struct{}

    func main() {
    set := make(Set)

    for _, item := range []string{"A", "A", "B", "C"} {
    set[item] = struct{}{}
    }
    fmt.Println(len(set)) // 3
    if _, ok := set["A"]; ok {
    fmt.Println("A exists") // A exists
    }
    }
  • 有时候给通道发送一个空结构体,channel<-struct{}{},也是节省了空间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func main() {
    ch := make(chan struct{}, 1)
    go func() {
    <-ch
    // do something
    }()
    ch <- struct{}{}
    // ...
    }
  • 仅有方法的结构体

1
type Lamp struct{}

go里面的int和int32是同一个概念吗?

不是一个概念!千万不能混淆。

go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统,int类型的大小就是4字节。如果是64位操作系统,int类型的大小就是8个字节。除此之外uint也与操作系统有关。

int8占1个字节,int16占2个字节,int32占4个字节,int64占8个字节。