Appearance
Go 编程题与代码分析
来源:用户提供的《golang 云原生面试题》PDF。下列为原文完整导入,按主题重排;页码为 PDF 物理页码。技术观点与示例保留原件语境,勘误请结合 校正说明 阅读。
看下面代码的 defer 的执行顺序是什么? defer 的作用和特点是什么?
PDF 出处:第 4–5 页 · 原卷分组:golang基础 · 源标题
p004-h04。
defer 的作用是:
你只需要在调用普通函数或方法前加上关键字 defer,就完成了 defer 所需要的语法。
当 defer 语句被执行时,跟在 defer 后面的函数会被延迟执行。
直到包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含 defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。
你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。
defer 的常用场景:
defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。
通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。
释放资源的 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。
- 问题描述
使用两个 goroutine 交替打印序列,一个 goroutine 打印数字,另外一个 goroutine 打印字母,最终效果如下:
text
12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728- 解题思路
问题很简单,使用 channel 来控制打印的进度。使用两个 channel,来分别控制数字和字母的打印序列,数字打印完成后通过 channel 通知字母打印, 字母打印完成后通知数字打印,然后周而复始的工作。
- 源码参考
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()- 源码解析
这里用到了两个 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。
- 问题描述
请实现一个算法,确定一个字符串的所有字符【是否全都不同】。这里我们要求【不允许使用额外的存储结构】。给定一个string,请返回一个bool值,true代表所有字符全都不同,false代表存在相同的字符。保证字符串中的字符为【ASCII字符】。字符串的⻓度小于等于【3000】
- 解题思路
这里有几个重点,第一个是 ASCII字符, ASCII字符字符一共有256个,其中128个是常用字符,可以在键盘上输入。128之后的是键盘上无法找到的。然后是全部不同,也就是字符串中的字符没有重复的,再次,不准使用额外的储存结构,且字符串小于等于3000。如果允许其他额外储存结构,这个题目很好做。如果不允许的话,可以使用golang内置的方式实现。
- 源码参考
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
}- 源码解析
以上两种方法都可以实现这个算法。第一个方法使用的是golang内置方法 strings.Count ,可以用来判断在一个字符串中包含的另外一个字符串的数量。第二个方法使用的是golang内置方法 strings.Index 和strings.LastIndex,用来判断指定字符串在另外一个字符串的索引未知,分别是第一次发现位置和最后发现位置
翻转字符串
PDF 出处:第 10–10 页 · 原卷分组:代码分析 · 源标题
p010-h01。
- 问题描述
请实现一个算法,在不使用【额外数据结构和储存空间】的情况下,翻转一个给定的字符串(可以使用单个过程变量)。给定一个string,请返回一个string,为翻转后的字符串。保证字符串的⻓度小于等于5000。
- 解题思路
翻转字符串其实是将一个字符串以中间字符为轴,前后翻转,即将str[len]赋值给str[0], 将str[0] 赋值str[len]。
- 源码参考:
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/2为轴,前后赋值
翻转含有中文、数字、英文字母的字符串
PDF 出处:第 11–11 页 · 原卷分组:代码分析 · 源标题
p011-h01。
- 解题思路
解题思路翻转字符串其实是将一个字符串以中间字符为轴,前后翻转
- 源码参考
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
}- 源码解析
rune关键字,从golang源码中看出,它是int32的别名(-2^31 ~ 2^31-1),比起byte(-128~127),可表示更多的字符。由于rune可表示的范围更大,所以能处理一切字符,当然也包括中文字符。在平时计算中文字符,可用rune。因此将字符串转为rune的切片
判断两个给定的字符串排序后是否一致
PDF 出处:第 11–12 页 · 原卷分组:代码分析 · 源标题
p011-h02。
- 问题描述
给定两个字符串,请编写程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。这里规定【大小写为不同字符】,且考虑字符串重点空格。给定一个string s1和一个string s2,请返回一个bool,代表两串是否重新排列后可相同。保证两串的⻓度都小于等于5000。
- 解题思路
首先要保证字符串⻓度小于5000。之后只需要一次循环遍历s1中的字符在s2是否都存在即可。
- 源码参考
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
}- 源码解析
使用golang内置方法 strings.Count 来判断字符是否一致
字符串替换问题
PDF 出处:第 12–12 页 · 原卷分组:代码分析 · 源标题
p012-h01。
- 问题描述
请编写一个方法,将字符串中的空格全部替换为“%20”。假定该字符串有足够的空间存放新增的字符,并且知道字符串的真实⻓度(小于等于1000),同时保证字符串由【大小写的英文字母组成】。给定一个string为原始的串,返回替换后的string。
- 解题思路
两个问题,第一个是只能是英文字母,第二个是替换空格。
- 源码参考
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
}- 源码解析
使用了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。
- 问题描述
有一个机器人,给一串指令,L左转 R右转,F前进一步,B后退一步,问最后机器人的坐标,最开始,机器人位于 0 0,方向为正Y。可以输入重复指令n:比如 R2(LF) 这个等于指令 RLFLF。问最后机器人的坐标是多少?
- 解题思路
这里的一个难点是解析重复指令。主要指令解析成功,计算坐标就简单了
- 源码参考
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
}- 源码解析
这里使用三个值表示机器人当前的状况,分别是:x表示x坐标,y表示y坐标,z表示当前方向。 L、R 命令会改变值z,F、B命令会改变值x、y。值x、y的改变还受当前的z值影响。如果是重复指令,那么将重复次数和重复的指令存起来递归调用即可
字符串转成byte切片,无内存拷贝的实现
PDF 出处:第 15–16 页 · 原卷分组:代码分析 · 源标题
p015-h02。
- 源码参考
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)))
}- 源码分析
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
}解析共发现两个问题:
main 函数不能加数字。
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 intGo 语言当中值传递和地址传递(引用传递)如何运用?有什么区别?举例说明
PDF 出处:第 23–23 页 · 原卷分组:代码分析 · 源标题
p023-h03。
值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同,不可相互修改。
地址传递(引用传递)会将变量本身传入对应的函数,在函数中可以对该变量进行值内容的修改。
写出打印的结果
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 的标签。