Questions - syntax
=
和 :=
的区别
1. := 声明+赋值
= 仅赋值
var foo int
foo = 10
// equal to
foo := 10
2. 指针的作用
指针用来保存变量的地址。
var x = 5
var p *int = &x
fmt.Printf("x = %d", *p) // x 可以用 *p 访问
*
:指针解引用,获取指针指向的值&
:获取变量地址
3. Go 允许多个返回值吗
允许
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("A", "B")
fmt.Println(a, b) // B A
}
4. Go 有异常类型吗
没有
只有错误类型error
,用于表示异常状态
f, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
5. 什么是协程(Goroutine)
Goroutine 是与其他函数或方法同时运行的函数或方法。 Goroutines 可以被认为是轻量级的线程。 与线程相比,创建 Goroutine 的开销很小。 Go应用程序同时运行数千个 Goroutine 是非常常见的做法。
6. 如何高效地拼接字符串
Go 语言中,字符串是只读的,也就意味着每次修改操作都会创建一个新的字符串。如果需要拼接多次,应使用 strings.Builder
,最小化内存拷贝次数。
var str strings.Builder
for i := 0; i < 1000; i++ {
str.WriteString("a")
}
fmt.Println(str.String())
7. 什么是 rune 类型
ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符。
为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。
Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。例如下面的例子中 语
和 言
使用 UTF-8 编码后各占 3 个 byte,因此 len("Go语言")
等于 8,当然我们也可以将字符串转换为 rune 序列。
fmt.Println(len("Go语言")) // 8
fmt.Println(len([]rune("Go语言"))) // 4
8. 如何判断 map 中是否包含某个 key
if val, ok := dict["foo"]; ok {
//do something here
}
ok
:表示 key 是否存在val
:key 对应的 value
9. Go 支持默认参数或可选参数吗
Go 语言不支持可选参数(python 支持),也不支持方法重载(java支持)
10. 简述 defer 的特性
- 执行顺序: 多个 defer 遵循 LIFO(Last In First Out,后进先出)的顺序执行,先定义的 defer 语句后执行
- 执行时机: defer 在 return 之后执行
- 参数预计算: 直接使用 defer 调用函数,函数参数会在定义时进行计算并拷贝,导致结果不符合预期; 应将函数放入闭包中执行
func tst() {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
fmt.Println("tst return ")
}
func main() {
tst()
}
// output
tst return
defer 2
defer 1
由上可以看出,执行顺序为 LIFO并且在 return 之后执行
func main() {
s1 := time.Now()
defer fmt.Println("Duration-1:", time.Since(s1))
s2 := time.Now()
defer func() {
fmt.Println("Duration-2:", time.Since(s2))
}()
time.Sleep(time.Second)
}
// output
Duration-2: 1.0057363s
Duration-1: 0s
defer fmt.Println("Duration-1:", time.Since(s1))
:此处在定义时,参数time.Since(s1)
已经进行计算完毕,导致计算的时间差为 0
11. defer 能否修改返回值
可以,当函数返回值为:
- 具名返回值
- 指针类型
func tst() int {
i := 0
defer func() {
i++
}()
return i
}
func tst1() (i int) {
defer func() {
i++
}()
return
}
func tst2() *int {
i := 0
defer func() {
i++
}()
return &i
}
func main() {
fmt.Println(tst()) // 0
fmt.Println(tst1()) // 1
fmt.Println(*tst2()) // 1
}
tst
中,返回的值已经经过拷贝,在defer中修改局部变量不会影响返回值。
12. 如何交换两个变量
a, b := 1, 2
a, b = b, a
fmt.Println(a, b) // 2, 1
13. 结构体中 tag 的作用
tag 可以作为字段的注解,利用反射可以获取 tag 的值。
框架/工具可以通过反射获取到某个字段定义的属性,采取相应的处理方式。tag 丰富了代码的语义,增强了灵活性。
type Stu struct {
Name string `json:"stu_name"`
ID string `json:"stu_id"`
Age string `json:"-"`
}
func main() {
buf, _ := json.Marshal(Stu{Name: "Alice", ID: "001", Age: "16"})
// {"stu_name":"Alice","stu_id":"001"}
fmt.Println(string(buf))
}
json:"stu_name"
:在编码成 JSON 格式时,将字段名改成stu_name
json:"-"
:JSON 编码时,跳过此字段
14. 如何判断切片内容是否相等
go 语言中可以使用反射 reflect.DeepEqual(a, b)
判断 a、b 两个切片是否相等,但是通常不推荐这么做,使用反射非常影响性能。
建议使用遍历的方式(性能比较):
func sliceEqual(a1, a2 []int) bool {
if len(a1) != len(a2) {
return false
}
// 异或运算
// 参与比较的切片不能为 nil
if (a1 == nil) != (a2 == nil) {
return false
}
for i, v := range a1 {
if a2[i] != v {
return false
}
}
return true
}
%v
, %+v
,%#v
区别
15. 格式化输出中的 func main() {
type s struct {
a int
b int
}
sv := s{
a: 1,
b: 2,
}
fmt.Printf("%v\n", sv) // {1 2}
fmt.Printf("%+v\n", sv) // {a:1 b:2}
fmt.Printf("%#v\n", sv) // main.s{a:1, b:2}
}
%v
:打印值%+v
:打印字段名和值%#v
:打印类型名及字段名
16. 如何定义枚举
使用 const 关键字和 iota
定义枚举值:
type StuType int32
const (
Type1 StuType = iota
Type2
Type3
Type4
)
func main() {
fmt.Println(Type1, Type2, Type3, Type4) // 0, 1, 2, 3
}
iota
初始值为 0,后续定义的枚举将会自动加一。
17. 空结构体的作用
空结构体struct{}{}
,用作占位符,因为其占用内存大小为0。
fmt.Println(unsafe.Sizeof(struct{}{})) // 0
配合 map 类型可以实现 set:
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 的元素类型时,可以只用于发送/接收信号:
func main() {
ch := make(chan struct{}, 1)
go func() {
<-ch
// do something
}()
ch <- struct{}{}
// ...
}
18. new 和 make 区别
- new 仅用于分配内存,返回指向变量的指针
- make 用于 slice, map 和 channel 的初始化
19. Golang 如何实现面向对象
- 封装:同一各包,对象对包内可见;不同的包,仅有导出的对象可见
- 继承:通过 struct 嵌套实现继承,可实现多重继承
- 多态:通过接口类型 interface 实现
20. uint 型变量相减会发生什么
对于变量 a, b uint
- 若
a > b
,得到正常值 - 若
a < b
,则发生溢出
func main() {
var a, b uint
a = 5
b = 8
fmt.Println(b - a) // 3
fmt.Println(a - b) // 18446744073709551613
}