Skip to content

Go 编程题与代码分析 ​

返回完整题目索引

来源:用户提供的《golang 云原生面试题》PDF。下列为原文完整导入,按主题重排;页码为 PDF 物理页码。技术观点与示例保留原件语境,勘误请结合 校正说明 阅读。

看下面代码的 defer 的执行顺序是什么? defer 的作用和特点是什么? ​

PDF 出处:第 4–5 页 · 原卷分组:golang基础 · 源标题 p004-h04。

defer 的作用是:

  1. 你只需要在调用普通函数或方法前加上关键字 defer,就完成了 defer 所需要的语法。

  2. 当 defer 语句被执行时,跟在 defer 后面的函数会被延迟执行。

  3. 直到包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含 defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。

  4. 你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。

defer 的常用场景:

  1. defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。

  2. 通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。

  3. 释放资源的 defer 应该直接跟在请求资源的语句后

下面的代码是有问题的,请说明原因 ​

PDF 出处:第 7–7 页 · 原卷分组:代码分析 · 源标题 p007-h03。

go
type People struct {
	Name string
}

func (p *People) String() string {
	return fmt.Sprintf("print: %v", p)
}
func main() {
	p := &People{}
	p.String()
}

解析:在golang中 String() string 方法实际上是实现了 String 的接口的,该接口定义在 fmt/print.go中:

go
type Stringer interface {
	String() string
}

在使用 fmt 包中的打印方法时,如果类型实现了这个接口,会直接调用。而题目中打印 p 的时候会直接调用 p 实现的 String() 方法,然后就产生了循环调用

交替打印数字和字母 ​

PDF 出处:第 7–8 页 · 原卷分组:代码分析 · 源标题 p007-h04。

  1. 问题描述

使用两个 goroutine 交替打印序列,一个 goroutine 打印数字,另外一个 goroutine 打印字母,最终效果如下:

text
12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728
  1. 解题思路

问题很简单,使用 channel 来控制打印的进度。使用两个 channel,来分别控制数字和字母的打印序列,数字打印完成后通过 channel 通知字母打印, 字母打印完成后通知数字打印,然后周而复始的工作。

  1. 源码参考
go
letter, number := make(chan bool), make(chan bool)
wait := sync.WaitGroup{}
go func() {
	i := 1
	for {
		select {
		case <-number:
			fmt.Print(i)
			i++
			fmt.Print(i)
			i++
			letter <- true
			break
		default:
			break
		}
	}
}()
wait.Add(1)
go func(wait *sync.WaitGroup) {
	str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	i := 0
	for {
		select {
		case <-letter:
			if i >= strings.Count(str, "")-1 {
				wait.Done()
				return
			}
			fmt.Print(str[i : i+1])
			i++
			if i >= strings.Count(str, "") {
				i = 0
			}
			fmt.Print(str[i : i+1])
			i++
			number <- true
			break
		default:
			break
		}
	}
}(&wait)
number <- true
wait.Wait()
  1. 源码解析

这里用到了两个 channel 负责通知,letter负责通知打印字母的goroutine来打印字母, number用来通知打印数字的goroutine打印数字。 wait用来等待字母打印完成后退出循环。

下列代码是否会触发异常 ​

PDF 出处:第 9–9 页 · 原卷分组:代码分析 · 源标题 p009-h01。

go
func Test59(t *testing.T) {
	runtime.GOMAXPROCS(1)
	intChan := make(chan int, 1)
	stringChan := make(chan string, 1)
	intChan <- 1
	stringChan <- "hello"
	select {
	case value := <-intChan:
		fmt.Println(value)
	case value := <-stringChan:
		panic(value)
	}
}

不一定,当两个chan同时有值时,select 会随机选择一个可用通道做收发操作

如何在运行时检查变量类型? ​

PDF 出处:第 9–9 页 · 原卷分组:代码分析 · 源标题 p009-h02。

类型开关(Type Switch)是在运行时检查变量类型的最佳方式。类型开关按类型而不是值来评估变量。每个 Switch 至少包含一个 case 用作条件语句,如果没有一个 case 为真,则执行 default。

go
var value interface{}
value = "abcd"

switch value.(type) {
case string:
	fmt.Println("string")
case int:
	fmt.Println("int")
case bool:
	fmt.Println("bool")
default:
	fmt.Println("default")
}

判断字符串中字符是否全都不同 ​

PDF 出处:第 9–10 页 · 原卷分组:代码分析 · 源标题 p009-h03。

  1. 问题描述

请实现一个算法,确定一个字符串的所有字符【是否全都不同】。这里我们要求【不允许使用额外的存储结构】。给定一个string,请返回一个bool值,true代表所有字符全都不同,false代表存在相同的字符。保证字符串中的字符为【ASCII字符】。字符串的⻓度小于等于【3000】

  1. 解题思路

这里有几个重点,第一个是 ASCII字符, ASCII字符字符一共有256个,其中128个是常用字符,可以在键盘上输入。128之后的是键盘上无法找到的。然后是全部不同,也就是字符串中的字符没有重复的,再次,不准使用额外的储存结构,且字符串小于等于3000。如果允许其他额外储存结构,这个题目很好做。如果不允许的话,可以使用golang内置的方式实现。

  1. 源码参考
go
func isUniqueString(s string) bool {
	if strings.Count(s, "") > 3000 {
		return false
	}
	for _, v := range s {
		if v > 127 {
			return false
		}
		if strings.Count(s, string(v)) > 1 {
			return false
		}
	}
	return true
}
  1. 源码解析

以上两种方法都可以实现这个算法。第一个方法使用的是golang内置方法 strings.Count ,可以用来判断在一个字符串中包含的另外一个字符串的数量。第二个方法使用的是golang内置方法 strings.Index 和strings.LastIndex,用来判断指定字符串在另外一个字符串的索引未知,分别是第一次发现位置和最后发现位置

翻转字符串 ​

PDF 出处:第 10–10 页 · 原卷分组:代码分析 · 源标题 p010-h01。

  1. 问题描述

请实现一个算法,在不使用【额外数据结构和储存空间】的情况下,翻转一个给定的字符串(可以使用单个过程变量)。给定一个string,请返回一个string,为翻转后的字符串。保证字符串的⻓度小于等于5000。

  1. 解题思路

翻转字符串其实是将一个字符串以中间字符为轴,前后翻转,即将str[len]赋值给str[0], 将str[0] 赋值str[len]。

  1. 源码参考:
go
func reverString(s string) (string, bool) {
	str := []rune(s)
	l := len(str)
	if l > 5000 {
		return s, false
	}
	for i := 0; i < l/2; i++ {
		str[i], str[l-1-i] = str[l-1-i], str[i]
	}
	return string(str), true
}
  1. 源码解析:

以字符串⻓度的1/2为轴,前后赋值

翻转含有中文、数字、英文字母的字符串 ​

PDF 出处:第 11–11 页 · 原卷分组:代码分析 · 源标题 p011-h01。

  1. 解题思路

解题思路翻转字符串其实是将一个字符串以中间字符为轴,前后翻转

  1. 源码参考
go
package main

import "fmt"

func main() {
	src := "nick 今天天气很好啊!1234"
	dst := reverse([]rune(src))
	fmt.Printf("%v\n", string(dst))
}
func reverse(s []rune) []rune {
	length := len(s)
	for i := 0; i < length/2; i++ {
		s[i], s[length-1-i] = s[length-1-i], s[i]
	}
	return s
}
  1. 源码解析

rune关键字,从golang源码中看出,它是int32的别名(-2^31 ~ 2^31-1),比起byte(-128~127),可表示更多的字符。由于rune可表示的范围更大,所以能处理一切字符,当然也包括中文字符。在平时计算中文字符,可用rune。因此将字符串转为rune的切片

判断两个给定的字符串排序后是否一致 ​

PDF 出处:第 11–12 页 · 原卷分组:代码分析 · 源标题 p011-h02。

  1. 问题描述

给定两个字符串,请编写程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。这里规定【大小写为不同字符】,且考虑字符串重点空格。给定一个string s1和一个string s2,请返回一个bool,代表两串是否重新排列后可相同。保证两串的⻓度都小于等于5000。

  1. 解题思路

首先要保证字符串⻓度小于5000。之后只需要一次循环遍历s1中的字符在s2是否都存在即可。

  1. 源码参考
go
func isRegroup(s1, s2 string) bool {
	sl1 := len([]rune(s1))
	sl2 := len([]rune(s2))
	if sl1 > 5000 || sl2 > 5000 || sl1 != sl2 {
		return false
	}
	for _, v := range s1 {
		if strings.Count(s1, string(v)) != strings.Count(s2, string(v)) {
			return false
		}
	}
	return true
}
  1. 源码解析

使用golang内置方法 strings.Count 来判断字符是否一致

字符串替换问题 ​

PDF 出处:第 12–12 页 · 原卷分组:代码分析 · 源标题 p012-h01。

  1. 问题描述

请编写一个方法,将字符串中的空格全部替换为“%20”。假定该字符串有足够的空间存放新增的字符,并且知道字符串的真实⻓度(小于等于1000),同时保证字符串由【大小写的英文字母组成】。给定一个string为原始的串,返回替换后的string。

  1. 解题思路

两个问题,第一个是只能是英文字母,第二个是替换空格。

  1. 源码参考
go
func replaceBlank(s string) (string, bool) {
	if len([]rune(s)) > 1000 {
		return s, false
	}
	for _, v := range s {
		if string(v) != " " && unicode.IsLetter(v) == false {
			return s, false
		}
	}
	return strings.Replace(s, " ", "%20", -1), true
}
  1. 源码解析

使用了golang内置方法 unicode.IsLetter 判断字符是否是字母,之后使用 strings.Replace 来替换空格

下列代码有什么问题 ​

PDF 出处:第 13–13 页 · 原卷分组:代码分析 · 源标题 p013-h01。

go
const i = 100

var j = 123

func main() {
	fmt.Println(&j, j)
	fmt.Println(&i, i)
}

go语言中,常量无法寻址,是不能进行取地址操作的

下列代码输出什么 ​

PDF 出处:第 13–13 页 · 原卷分组:代码分析 · 源标题 p013-h02。

go
func Test62(t *testing.T) {
	x := []string{"a", "b", "c"}
	for v := range x {
		fmt.Print(v)
	}
}

对于切片,range一个返回值时,返回的是切片的下标即索引。返回两个值时,第一个是下标,第二个是值。对于集合,range一个返回值时,返回的是集合的key值。返回两个值时,第一个是key,第二个是值因此,以上代码输出 012

下列代码输出什么 ​

PDF 出处:第 13–13 页 · 原卷分组:代码分析 · 源标题 p013-h03。

go
func main() {
	var x *int = nil
	Foo(x)
}
func Foo(x interface{}) {
	if x == nil {
		fmt.Println("empty interface")
		return
	}
	fmt.Println(x)
	fmt.Println("non-empty interface")
}

机器人坐标问题 ​

PDF 出处:第 13–15 页 · 原卷分组:代码分析 · 源标题 p013-h04。

  1. 问题描述

有一个机器人,给一串指令,L左转 R右转,F前进一步,B后退一步,问最后机器人的坐标,最开始,机器人位于 0 0,方向为正Y。可以输入重复指令n:比如 R2(LF) 这个等于指令 RLFLF。问最后机器人的坐标是多少?

  1. 解题思路

这里的一个难点是解析重复指令。主要指令解析成功,计算坐标就简单了

  1. 源码参考
go
package main

import (
	"unicode"
)

const (
	Left = iota
	Top
	Right
	Bottom
)

func main() {
	println(move("R2(LF)", 0, 0, Top))
}
func move(cmd string, x0 int, y0 int, z0 int) (x, y, z int) {
	x, y, z = x0, y0, z0
	repeat := 0
	repeatCmd := ""
	for _, s := range cmd {
		switch {
		case unicode.IsNumber(s):
			repeat = repeat*10 + (int(s) - '0')
		case s == ')':
			for i := 0; i < repeat; i++ {
				x, y, z = move(repeatCmd, x, y, z)
			}
			repeat = 0
			repeatCmd = ""
		case repeat > 0 && s != '(' && s != ')':
			repeatCmd = repeatCmd + string(s)
		case s == 'L':
			z = (z + 1) % 4
		case s == 'R':
			z = (z - 1 + 4) % 4
		case s == 'F':
			switch {
			case z == Left || z == Right:
				x = x - z + 1
			case z == Top || z == Bottom:
				y = y - z + 2
			}
		case s == 'B':
			switch {
			case z == Left || z == Right:
				x = x + z - 1
			case z == Top || z == Bottom:
				y = y + z - 2
			}
		}
	}
	return
}
  1. 源码解析

这里使用三个值表示机器人当前的状况,分别是:x表示x坐标,y表示y坐标,z表示当前方向。 L、R 命令会改变值z,F、B命令会改变值x、y。值x、y的改变还受当前的z值影响。如果是重复指令,那么将重复次数和重复的指令存起来递归调用即可

字符串转成byte切片,无内存拷贝的实现 ​

PDF 出处:第 15–16 页 · 原卷分组:代码分析 · 源标题 p015-h02。

  1. 源码参考
go
package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	a := "abc"
	ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a))
	fmt.Printf("%+v\n", ssh)
	b := *(*[]byte)(unsafe.Pointer(&ssh))
	fmt.Printf("%+v\n", b)
	fmt.Printf("%+v\n", *(*reflect.SliceHeader)(unsafe.Pointer(&b)))
}
  1. 源码分析

StringHeader 是字符串在go的底层结构

go
type StringHeader struct {
	Data uintptr
	Len  int
}

SliceHeader 是切片在go的底层结构

go
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

在底层转换二者,只需要把 StringHeader 的地址强转成 SliceHeader 就行。

unsafe.Pointer(&a)方法可以得到变量a的地址(reflect.StringHeader)(unsafe.Pointer(&a)) 可以把字符串a转成底层结构的形式([]byte)(unsafe.Pointer(&ssh)) 可以把ssh底层结构体转成byte的切片的指针再通过 *转为指针指向的实际内容

下列代码会造成死循环吗? ​

PDF 出处:第 17–17 页 · 原卷分组:代码分析 · 源标题 p017-h01。

go
package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4, 5}
	for _, v := range s {
		s = append(s, v)
		fmt.Printf("len(s)=%v\n", len(s))
	}
}

不会死循环,for range其实是golang的语法糖,在循环开始前会获取切片的长度 len(切片),然后再执行len(切片)次数的循环

下面代码能运行吗?为什么 ​

PDF 出处:第 22–22 页 · 原卷分组:代码分析 · 源标题 p022-h02。

go
type Param map[string]interface{}
type Show struct {
	Param
}

func main1() {
	s := new(Show)
	s.Param["RMB"] = 10000
}

解析共发现两个问题:

  1. main 函数不能加数字。

  2. new 关键字无法初始化 Show 结构体中的 Param 属性,所以直接对 s.Param 操作会出错

请说出下面代码存在什么问题 ​

PDF 出处:第 23–23 页 · 原卷分组:代码分析 · 源标题 p023-h01。

go
type student struct {
	Name string
}

func zhoujielun(v interface{}) {
	switch msg := v.(type) {
	case *student, student:
		msg.Name
	}
}

解析: golang中有规定, switch type 的 case T1,类型列表只有一个,那么 v := m.(type) 中的 v 的类型就是T1类型。如果是 case T1, T2,类型列表中有多个,那 v 的类型还是多对应接口的类型,也就是 m 的类型。所以这里 msg 的类型还是 interface{},所以他没有 Name 这个字段,编译阶段就会报错。具体解释⻅:https://golang.org/ref /spec#Type_switches

Go 支持什么形式的类型转换?将整数转换为浮点数 ​

PDF 出处:第 23–23 页 · 原卷分组:代码分析 · 源标题 p023-h02。

Go 支持显式类型转换以满足其严格的类型要求。

go
i := 55           //int
j := 67.8         //float64
sum := i + int(j) //j is converted to int

Go 语言当中值传递和地址传递(引用传递)如何运用?有什么区别?举例说明 ​

PDF 出处:第 23–23 页 · 原卷分组:代码分析 · 源标题 p023-h03。

  1. 值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同,不可相互修改。

  2. 地址传递(引用传递)会将变量本身传入对应的函数,在函数中可以对该变量进行值内容的修改。

写出打印的结果 ​

PDF 出处:第 23–24 页 · 原卷分组:代码分析 · 源标题 p023-h04。

go
type People struct {
	name string `json:"name"`
}

func main() {
	js := `{
"name":"11"
}`
	var p People
	err := json.Unmarshal([]byte(js), &p)
	if err != nil {
		fmt.Println("err: ", err)
		return
	}
	fmt.Println("people: ", p)
}

解析:按照 golang 的语法,小写开头的方法、属性或 struct 是私有的,同样,在 json 解码或转码的时候也无法上线私有属性的转换。题目中是无法正常得到 People 的 name 值的。而且,私有属性 name 也不应该加 json 的标签。