常见面试题目

🖥️ 交替打印奇数和偶数

package main

import (
   "fmt"
   "sync"
)

func main() {
   // 正确写法:有缓冲管道(不会发生阻塞)
   // oddChan := make(chan struct{}, 1)  // 奇数
   // evenChan := make(chan struct{}, 1) // 偶数
   // 错误写法:无缓冲管道(会发生阻塞)
   oddChan := make(chan struct{})  // 奇数
   evenChan := make(chan struct{}) // 偶数

   // 使用WaitGroup等待所有协程完成
   waitGroup := sync.WaitGroup{}
   waitGroup.Add(2)

   // 启动输出奇数的协程
   go func() {
      defer waitGroup.Done()
      for i := 1; i <= 10; i += 2 {
         <-oddChan // 等待奇数通道信号
         fmt.Printf("奇数:%d \n", i)
         evenChan <- struct{}{} // 发送偶数通道信号
      }
   }()

   // 启动输出偶数的协程
   go func() {
      defer waitGroup.Done()
      for i := 2; i <= 10; i += 2 {
         <-evenChan // 等待偶数通道信号
         fmt.Printf("偶数:%d \n", i)
         oddChan <- struct{}{} // 发送奇数通道信号
      }
   }()

   oddChan <- struct{}{} // 发送奇数通道信号,解除输出奇数协程的阻塞,先输出奇数1

   waitGroup.Wait() // 阻塞主线程,等待所有协程完成才解除阻塞

   close(oddChan)
   close(evenChan)
}

// ========== 总结 ========== //
// 1、上面的代码的确可以让两个协程交替输出1~10,即一个输出奇数,一个输出偶数,但是输出完数字后会报错:
//    fatal error: all goroutines are asleep - deadlock!
//    从详细错误信息可知问题出在“oddChan <- struct{}{}”这行代码,由于是deadlock所以可以肯定是向oddChan写数据阻塞了。
//
//    问题分析:当输出奇数协程输出了“奇数:9”后,它的for循环就执行完毕了,此时oddChan管道就完全没有数据接收方了,
//              但输出偶数协程输出了“偶数:10”后仍然向oddChan发信号,而oddChan是无缓冲管道,由于没有数据接收方,
//              所以“oddChan <- struct{}{}”会一直阻塞导致死锁。
//
//    解决办法:最简单的做法是将两个无缓冲管道改为有缓冲管道(容量为1即可),这样“oddChan <- struct{}{}”
//              就不会发生阻塞了。



🖥️ 交替打印数字和字母

package main

import (
   "fmt"
   "strings"
   "sync"
)

// 问题描述
// ----------------------------------------------------------------------------------------------------
// 使两个goroutine交替打印序列,个goroutine打印数字,另外个goroutine打印字母,最终效果如下:
// 12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728

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

func main() {
   letter := make(chan bool)
   number := make(chan bool)

   wait := sync.WaitGroup{}

   // 这个协程打印数字
   go func() {
      i := 1
      for {
         select {
         case <-number: // 收到通知,开始打印两个数字
            fmt.Print(i) // 打印数字
            i++

            fmt.Print(i) // 打印数字
            i++

            letter <- true // 已经打印出两个数字,向letter管道写入数据,通知负责打印字母的协程开始打印字母
            break
         default:
            break
         }
      }
   }()

   wait.Add(1) // 计数器加1

   // 这个协程打印字母
   go func(wait *sync.WaitGroup) {
      str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      i := 0
      for {
         select {
         case <-letter: // 收到通知,开始打印两个字母
            // 判断是否已打印完26个字母,如果已打印完则停止协程任务
            if i >= strings.Count(str, "")-1 {
               wait.Done() // 计数器减1,此时计数器将归零,然后解除wait.Wait()的阻塞,整个程序执行完毕
               return
            }

            fmt.Print(str[i : i+1]) // 打印字母
            i++

            // 这里检查一下i的值避免越界
            if i >= strings.Count(str, "") {
               i = 0
            }

            fmt.Print(str[i : i+1]) // 打印字母
            i++

            number <- true // 已经打印出两个字母,向number管道写入数据,通知负责打印数字的协程开始打印数字
            break
         default:
            break
         }
      }
   }(&wait)

   number <- true // 向number管道写入数据,通知负责打印数字的协程开始打印数字

   wait.Wait() // 在执行wait.Done()前这里会一直阻塞
}



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

package main

import (
   "fmt"
   "strings"
)

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

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

func isUniqueString(s string) bool {
   if strings.Count(s, "") > 3000 {
      return false
   }

   for _, v := range s {
      if v > 127 {
         return false
      }

      // 判断单个字符v在字符串s中的出现次数
      if strings.Count(s, string(v)) > 1 {
         return false
      }
   }

   return true
}

func isUniqueString2(s string) bool {
   if strings.Count(s, "") > 3000 {
      return false
   }

   for k, v := range s {
      if v > 127 {
         return false
      }

      // 判断单个字符v在字符串s中首次出现位置
      if strings.Index(s, string(v)) != k {
         return false
      }
   }

   return true
}

func main() {
   s := "ABCDEFGH"
   if isUniqueString(s) {
      fmt.Printf("『%s』字符全都不同 \n", s) // 『ABCDEFGH』字符全都不同
   } else {
      fmt.Printf("『%s』字符部分相同 \n", s)
   }

   s = "ABCDEFGC"
   if isUniqueString(s) {
      fmt.Printf("『%s』字符全都不同 \n", s)
   } else {
      fmt.Printf("『%s』字符部分相同 \n", s) // 『ABCDEFGC』字符部分相同
   }

   s = "ABCDEFGH"
   if isUniqueString2(s) {
      fmt.Printf("『%s』字符全都不同 \n", s) // 『ABCDEFGH』字符全都不同
   } else {
      fmt.Printf("『%s』字符部分相同 \n", s)
   }

   s = "ABCDEFGC"
   if isUniqueString2(s) {
      fmt.Printf("『%s』字符全都不同 \n", s)
   } else {
      fmt.Printf("『%s』字符部分相同 \n", s) // 『ABCDEFGC』字符部分相同
   }
}



🖥️ 翻转字符串

package main

import "fmt"

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

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

func revString(s string) (string, bool) {
   str := []int32(s)
   length := len(str)

   if length > 5000 {
      return s, false
   }

   for i := 0; i < length/2; i++ {
      str[i], str[length-1-i] = str[length-1-i], str[i] // 交换位置
   }

   return string(str), true
}

func main() {
   str := "TEST"
   rev, ok := revString(str)
   if ok {
      fmt.Printf("%s => %s", str, rev) // TEST => TSET
   } else {
      fmt.Print("字符串长度大于5000了~~~")
   }
}



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

package main

import (
   "fmt"
   "strings"
)

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

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

func isRegroup(s1, s2 string) bool {
   len1 := len([]int32(s1))
   len2 := len([]int32(s2))

   if len1 > 5000 || len2 > 5000 || len1 != len2 {
      return false
   }

   for _, v := range s1 {
      // 判断单个字符v在字符串s1中出现的次数和在字符串s2中出现的次数是否相等
      if strings.Count(s1, string(v)) != strings.Count(s2, string(v)) {
         return false
      }
   }

   return true
}

func main() {
   s1 := "foo"
   s2 := "ofo"
   fmt.Print(isRegroup(s1, s2)) // true
}



🖥️ 字符串替换问题

package main

import (
   "fmt"
   "strings"
   "unicode"
)

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

// 解题思路
// ----------------------------------------------------------------------------------------------------
// 两个问题,第一个是只能是英文字,第个是替换空格。

func replaceBlank(s string) (string, bool) {
   if len([]rune(s)) > 1000 {
      return s, false
   }

   for _, v := range s {
      // 判断单个字符v是否是一个字母(不区分大小写)
      if string(v) != " " && unicode.IsLetter(v) == false {
         return s, false
      }
   }

   // 把空格替换成“%20”,strings.Replace()的第三个参数表示替换次数,若为-1则表示全部替换
   return strings.Replace(s, " ", "%20", -1), true
}

func main() {
   s := "R U OK"
   r, ok := replaceBlank(s)
   if ok {
      fmt.Println(r) // R%20U%20OK
   } else {
      fmt.Println("字符串不合法")
   }
}



🖥️ 机器人坐标问题

package main

import (
   "fmt"
   "unicode"
)

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

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

// 说明
// ----------------------------------------------------------------------------------------------------
// 机器人只是在一个平面直角坐标系内移动,所以只有X轴和Y轴,没有Z轴,可以理解为机器人是扫地机器人,不是飞行机器人。
// 前进和后退不会改变方向。

// 定义四个方向的常量值:Top=0|Right=1|Bottom=2|Left=3
const (
   Top = iota
   Right
   Bottom
   Left
)

func move(cmd string, x0 int, y0 int, d0 int) (x, y, d int) {
   x, y, d = x0, y0, d0 // 初始化机器人的X坐标、Y坐标和方向

   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, d = move(repeatCmd, x, y, d)
         }
         repeat = 0
         repeatCmd = ""
      case repeat > 0 && s != '(' && s != ')':
         repeatCmd = repeatCmd + string(s)
      case s == 'L': // L会改变d值,即向左转会改变方向
         d = (d - 1 + 4) % 4
         // 0 -> 3  原方向为0(上),向左转后方向变为3(左)
         // 1 -> 0  原方向为1(右),向左转后方向变为0(上)
         // 2 -> 1  原方向为2(下),向左转后方向变为1(右)
         // 3 -> 2  原方向为3(左),向左转后方向变为2(下)
         fmt.Printf("机器人向左转后:%d, %d, %d \n", x, y, d)
      case s == 'R': // R会改变d值,即向右转会改变方向
         d = (d + 1) % 4
         // 0 -> 1  原方向为0(上),向右转后方向变为1(右)
         // 1 -> 2  原方向为1(右),向右转后方向变为2(下)
         // 2 -> 3  原方向为2(下),向右转后方向变为3(左)
         // 3 -> 0  原方向为3(左),向右转后方向变为0(上)
         fmt.Printf("机器人向右转后:%d, %d, %d \n", x, y, d)
      case s == 'F': // F会改变x值、y值,即前进一步会改变坐标
         switch {
         case d == Left || d == Right:
            x = x - d + 2 // 当前方向为向左或向右,前进一步就是X坐标减1或加1
         case d == Top || d == Bottom:
            y = y - d + 1 // 当前方向为向上或向下,前进一步就是Y坐标加1或减1
         }
         fmt.Printf("机器人往前走后:%d, %d, %d \n", x, y, d)
      case s == 'B': // B会改变x值、y值,即后退一步会改变坐标
         switch {
         case d == Left || d == Right:
            x = x + d - 2 // 当前方向为向左或向右,后退一步就是X坐标加1或减1
         case d == Top || d == Bottom:
            y = y + d - 1 // 当前方向为向上或向下,后退一步就是Y坐标减1或加1
         }
         fmt.Printf("机器人往后退后:%d, %d, %d \n", x, y, d)
      }
   }
   return
}

func main() {
   x, y, d := move("R2(LF)", 0, 0, Top) // 机器人初始位置为“0 0”,方向为向上
   fmt.Println(x, y, d)                 // -1 1 3
}



🖥️ 常见语法题(1):下⾯代码能运⾏吗,为什么?

package main

type Param map[string]interface{}

type Show struct {
   Param
}

func main() {
   s := new(Show)

   // 这里会报panic,因为上面的new(Show)只是初始化了Show结构体,并没有初始化它的Param字段
   s.Param["RMB"] = 10000

   // 以下才是正确写法
   // s.Param = map[string]interface{}{}
   // s.Param["RMB"] = 10000
}



🖥️ 常见语法题(2):请说出下面代码存在什么问题。

package main

type student struct {
   Name string
}

func ZhouJieLun(v interface{}) {
   switch msg := v.(type) {
   case *student, student:
      msg.Name // Unresolved reference 'Name'
   }
}

// ========== 说明 ========== //
// 1、上面的代码是无法通过编译的,因为case后面跟了“*student”和“student”两种数据类型,
//    此时case{}代码块里的msg是interface{}数据类型,也就是说msg根本没有Name字段,在编译时就会报错,
//    错误信息为“Unresolved reference 'Name'”。
// 2、使用GoLand编辑器时,把鼠标移到msg变量上面时可以清楚地看到“var msg interface{} = v”。



🖥️ 常见语法题(3):写出打印的结果。

package main

import (
   "encoding/json"
   "fmt"
)

type People struct {
   name string `json:"name"` // Struct field 'name' has 'json' tag but is not exported
   // Name string `json:"name"` // 将字段名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) // people:  {}
}

// ========== 说明 ========== //
//  1、People结构体的name字段有json标签但是没有导出,直白点说就是name字段的首字母为小写,外部无法直接访问,
//     所以json.Unmarshal()无法将JSON数据的值赋给name字段。解决办法就是将name字段导出,也就是将首字母改为大写。



🖥️ 常见语法题(4):下面的代码是有问题的,请说明原因。

package main

import "fmt"

type People struct {
   Name string
}

func (p *People) String() string {
   return fmt.Sprintf("print: %v", p) // Placeholder argument causes a recursive call to the 'String' method (%v)
}

func main() {
   p := &People{}
   p.String()
}

// ========== 说明 ========== //
// 1、占位符“%v”对应的是变量p,所以fmt.Sprintf()在打印“%v”时会调用p.String()方法来生成p的字符串表示,
//    从这里可以看出p.String()方法发生了递归调用,程序陷入死循环,然后报stack overflow(堆栈溢出)的panic。



🖥️ 常见语法题(5):请找出下面代码的问题所在。

package main

import (
   "fmt"
   "time"
)

func main() {
   ch := make(chan int, 1000)

   go func() {
      for i := 0; i < 10; i++ {
         ch <- i
      }
   }()

   go func() {
      for {
         a, ok := <-ch
         if !ok {
            fmt.Println("close")
            return
         }
         fmt.Println("a: ", a)
      }
   }()

   close(ch)

   fmt.Println("ok")
   time.Sleep(time.Second * 100)
}

// ========== 说明 ========== //
// 1、协程的调度是随机的,也就是说可能出现第一个协程还未执行,关闭管道操作“close(ch)”就已经执行的情况,
//    而向已关闭管道写会引发panic。



🖥️ 常见语法题(6):请说明下面代码书写是否正确。

package main

import (
   "fmt"
   "sync/atomic"
)

var value int32

func SetValue(delta int32) {
   for {
      v := value // 读取value的值

      // atomic.CompareAndSwapInt32()函数会检查value的值是否等于v,即检查value被读取后有没有被其它协程修改过,
      // 如果value等于v说明没有被修改过,这时value的值将设为v+delta,并且函数返回true,表示更新成功;
      // 否则不会修改value的值,并且函数返回false,表示更新失败。
      if atomic.CompareAndSwapInt32(&value, v, v+delta) {
         break
      }
   }
}

func main() {
   SetValue(9527)
   fmt.Printf("value的值为%d \n", value) // value的值为9527
}

// ========== 说明 ========== //
// 1、高并发环境下,在死循环里调用atomic.CompareAndSwapInt32()会造成大量的CPU资源浪费,
//    所以应该避免在for循环里调用atomic.CompareAndSwapInt32()。



🖥️ 常见语法题(7):下面的程序运行后为什么会爆异常。

package main

import (
   "fmt"
   "time"
)

type Project struct{}

func (p *Project) deferError() {
   if err := recover(); err != nil {
      fmt.Println("recover: ", err)
   }
}

func (p *Project) exec(msgchan chan interface{}) {
   for msg := range msgchan {
      m := msg.(int)
      fmt.Println("msg: ", m)
   }
}

func (p *Project) run(msgchan chan interface{}) {
   for {
      defer p.deferError() // 这里存在weak warning:Possible resource leak, 'defer' is called in the 'for' loop
      go p.exec(msgchan)
      time.Sleep(time.Second * 2)
   }
}

func (p *Project) Main() {
   a := make(chan interface{}, 100)
   go p.run(a)
   go func() {
      for {
         a <- "1"
         time.Sleep(time.Second)
      }
   }()
   time.Sleep(time.Second * 100000000000000)
}

func main() {
   p := new(Project)
   p.Main()
}

// ========== 说明 ========== //
// 1、上面代码的主要问题是在“time.Sleep(time.Second * 100000000000000)”这里,它的参数超出了int64最大值,错误信息如下:
//    time.Second * 100000000000000 (constant 100000000000000000000000 of type time.Duration) overflows int64
// 2、即便把time.Sleep()的参数改为合法数值,程序也还是会报panic,原因是在“a <- "1"”这里,这里往管道写入的是字符串1,
//    但是在exec()方法里遍历管道数据时却断言是int类型,并且是使用会报panic的断言写法。解决办法有两个,要么向管道写入
//    数据时将字符串1改为数字1,要么在断言时将int改为string。
// 3、此外run()方法也有问题,首先,run()方法和该方法内启动的协程不在同一个调用链里,defer p.deferError()是无法捕获错误的,
//    所以defer p.deferError()应该写在exec()方法里;其次,在for循环使用defer可能会造成资源泄漏(weak warning,非致命错误)。



🖥️ 常见语法题(8):请说出下面代码哪里写错了。

package main

import (
   "fmt"
   "time"
)

func main() {
   abc := make(chan int, 1000)

   for i := 0; i < 10; i++ {
      abc <- i
   }

   go func() {
      for a := range abc {
         fmt.Println("a: ", a)
      }
   }()

   close(abc)
   fmt.Println("close")

   time.Sleep(time.Second * 100)
}

// ========== 说明 ========== //
// 1、上面的代码并没有什么问题,功能很简单,就是创建一个容量为1000的int管道,然后往管道里写入0~9这十个数字,
//    接着创建一个协程,在协程里遍历int管道并打印数据。
//    这里可能会出现协程还未启动,管道就已经关闭的情况,但这是完全没有问题的,for-range可以遍历已关闭管道。
//    上面的代码可以改为往管道里写入0~9这十个数字后立即关闭管道,也不用创建协程,直接使用for-range遍历管道,
//    这样也还是能打印出0~9这十个数字,程序不会报任何错误。



🖥️ 常见语法题(9):请说出下面代码,执行时为什么会报错。

package main

type Student struct{ name string }

func main() {
   m := map[string]Student{"people": {"zhoujielun"}}
   m["people"].name = "wuyanzu" // cannot assign to struct field m["people"].name in map
}

// ========== 说明 ========== //
// 1、上面代码不是执行会报错,而是无法通过编译,编译时错误信息:cannot assign to struct field m["people"].name in map
//    变量m是一个keyType为string、valueType为Student结构体的map,保存在map里的value实际上是原始Student结构体的值拷贝,
//    而不是原始Student结构体的指针,所以通过“m["people"].name”的方式访问name字段是错误的。
//    解决这个问题最简单的方法是把map的valueType从Student改为*Student,这样就能直接访问name字段了,强烈推荐使用该方法。
//    另一个方法是先读取m["people"]的值到一个临时变量,通过临时变量修改name字段,然后再把临时变量保存回m["people"],代码如下:
//    people := m["people"]
//    people.name = "wuyanzu"
//    m["people"] = people



🖥️ 常见语法题(10):请说出下面的代码存在什么问题。

package main

import "fmt"

// 声明一个名为query的自定义数据类型,该类型是一个函数,函数形参是一个字符串,函数返回值也是一个字符串
type query func(string) string

func exec(name string, vs ...query) string {
   ch := make(chan string) // 创建一个无缓冲(即容量为0)的string类型管道

   // 定义一个匿名函数,并将其赋值给变量fn
   fn := func(i int) {
      ch <- vs[i](name)
      // vs[i]是一个函数,就是main()函数里调用exec()时传入的四个函数之一,具体是哪个由i的值决定,而name是参数
   }

   // 遍历可变参数vs,从main()函数可知vs长度为4,所以这里就是创建4个协程执行匿名函数fn,没有什么复杂的
   for i, _ := range vs {
      go fn(i)
   }
   // 上面的循环在效果上就是执行下面这些操作:
   // go ch <- vs[0]("111")  说明:vs[0]就是main()函数里调用exec()时传入的第1个函数
   // go ch <- vs[1]("111")  说明:vs[1]就是main()函数里调用exec()时传入的第2个函数
   // go ch <- vs[2]("111")  说明:vs[2]就是main()函数里调用exec()时传入的第3个函数
   // go ch <- vs[3]("111")  说明:vs[3]就是main()函数里调用exec()时传入的第4个函数
   // 所以最终效果就是:
   // go ch <- "111func1"
   // go ch <- "111func2"
   // go ch <- "111func3"
   // go ch <- "111func4"

   // 由于协程的调度是不确定的,所以“111func1”、“111func2”、“111func3”、“111func4”哪个先写入管道无法确定,
   // 那本函数的返回内容同样也无法确定。
   return <-ch
}
func main() {
   ret := exec("111",
      func(n string) string { return n + "func1" },
      func(n string) string { return n + "func2" },
      func(n string) string { return n + "func3" },
      func(n string) string { return n + "func4" })
   fmt.Println(ret)
}

// ========== 说明 ========== //
// 1、上面的代码没有什么问题,只是打印内容无法确定,很大概率会打印“111func4”,其次是“111func1”,
//    但也可能会打印“111func2”或“111func3”,但多次执行并没有发现打印“111func2”或“111func3”。
// 2、要说完全没问题也不妥,“for i, _ := range vs”里的下划线是多余的(编辑器报警告),可以去掉。



🖥️ 常见语法题(11):下面这段代码为什么会卡死?

package main

import (
   "fmt"
   "runtime"
)

func main() {
   var i byte

   go func() {
      // 重要说明:这里是死循环【Condition 'i <= 255' is always 'true' 】
      for i = 0; i <= 255; i++ {
      }
   }()

   fmt.Println("Dropping mic")

   runtime.Gosched() // 当前协程(此时为主线程)让出CPU使用权
   runtime.GC()      // 强制垃圾回收(重要说明:程序是卡在这里)

   fmt.Println("Done")
}

// ========== 说明 ========== //
// 1、首先要知道for循环是一个死循环,因为i的数据类型是byte,它有一个别名是uint8,其范围是0~255,当i达到255时再执行i++会变回0。
//    但导致程序卡死的主要原因并不是这个死循环,而是runtime.GC(),也就是强制垃圾回收,程序是卡在了这一步。
//    垃圾回收需要等协程执行完,但协程又是死循环,所以程序会一直卡在runtime.GC()这里。
// 2、runtime.Gosched()的作用是当前协程让出CPU使用权,它让不让对程序都没有影响。



🖥️ 常见语法题(12):写出下⾯代码输出内容。

package main

import (
   "fmt"
)

func main() {
   defer_call()
}

func defer_call() {
   defer func() { fmt.Println("打印前") }()
   defer func() { fmt.Println("打印中") }()
   defer func() { fmt.Println("打印后") }()
   panic("触发异常")
}

// 输出结果如下:
// --------------------------------------------------
// 打印后
// 打印中
// 打印前
// panic: 触发异常

// ========== 说明 ========== //
// 1、panic并不会中止已注册的defer,这些defer会按照后进先出的规则依次执行。



🖥️ 常见语法题(13):以下代码有什么问题,说明原因。

package main

type student struct {
   Name string
   Age  int
}

func pase_student() {
   m := make(map[string]*student)

   stus := []student{
      {Name: "zhou", Age: 24},
      {Name: "li", Age: 23},
      {Name: "wang", Age: 22},
   }

   // 【问题在for-range{}块里】
   // 这个for-range遍历切片是有问题的,stu是一个临时变量,并且在循环中会复用,并不是每次循环都重新创建,
   // 从这里就能知道,由始至终“m[stu.Name] = &stu”都是在保存stu这个临时变量的指针。在for-range执行完后打印m,
   // 就能清楚地能看到m["zhou"]、m["li"]、m["wang"]三个元素(指针)是同一个地址。
   // 由于stus是[]student类型,所以在每次循环里stu保存的是student结构体的值拷贝,而不是student结构体的指针,
   // 所以m["zhou"]、m["li"]、m["wang"]三个指针指向的值是最后一轮循环的student结构体的值,即{Name: "wang", Age: 22}。
   // 最简单的解决办法是把stus的数据类型改为“[]*student”,同时“m[stu.Name] = &stu”改为“m[stu.Name] = stu”。
   for _, stu := range stus {
      m[stu.Name] = &stu
   }

   // 打印一下数据就能看出问题(三个都是同一个内存地址)
   // fmt.Println(m)         // map[li:0xc000008078 wang:0xc000008078 zhou:0xc000008078]
   // fmt.Println(m["zhou"]) // &{wang 22}
   // fmt.Println(m["li"])   // &{wang 22}
   // fmt.Println(m["wang"]) // &{wang 22}
}

func main() {
   pase_student()
}



🖥️ 常见语法题(14):下面的代码会输出什么,并说明原因。

package main

import (
   "fmt"
   "runtime"
   "sync"
)

func main() {
   runtime.GOMAXPROCS(1)

   wg := sync.WaitGroup{}

   wg.Add(20)

   for i := 0; i < 10; i++ {
      go func() {
         fmt.Println("i: ", i)
         wg.Done()
      }()
   }

   for i := 0; i < 10; i++ {
      go func(i int) {
         fmt.Println("i: ", i)
         wg.Done()
      }(i)
   }

   wg.Wait()
}

// 结论:代码具体会输出什么内容无法确定,不过内容还是有一定的规律,具体看说明第三条。

// ========== 说明 ========== //

// 1、runtime.GOMAXPROCS(1)表示程序运行时系统可使用逻辑处理器数量为1,直白点说就是由一个核心处理所有协程,
//    必须注意的是,由一个核心处理所有协程并不意味着协程调度变得可预测或者说协程将变成顺序执行。
//    runtime.GOMAXPROCS(1)只是让协程之间的切换变得更频繁,因为每个协程只能运行一段时间,然后被迫让出CPU给其它协程,
//    无论如何协程的调度仍然是不可预测的。
// 2、上面的代码用两个for循环创建了20个协程(每个for循环各10个),由于协程的调度是无法确定的,所以输出结果无法确定,
//    多次执行上面的程序也能清楚地看到每次输出结果都可能不同。
// 3、(下面把第一个for循环创建的10个协程称为A10协程,第二个for循环创建的10个协程称为B10协程)
//    虽然A10协程和B10协程都是打印i的值,但两者是有区别的,A10协程打印的是外部变量i,而B10协程打印的是匿名函数参数i,
//    所以A10协程将会打印出外部变量i的即时值,而B10协程将会打印出匿名函数参数i的过程值。
//    这就导致了A10协程会打印出什么完全无法确定(看执行协程时外部变量i的即时值);而B10协程肯定会打印出0~9这10个数字,
//    但这10个数字的顺序是什么无法确定。



🖥️ 常见语法题(15):下面代码会输出什么?

package main

import "fmt"

type People struct{}

func (p *People) ShowA() {
   fmt.Println("showA")
   p.ShowB()
}

func (p *People) ShowB() {
   fmt.Println("showB")
}

type Teacher struct {
   People
}

func (t *Teacher) ShowB() {
   fmt.Println("teacher showB")
}

func main() {
   t := Teacher{}
   t.ShowA()
}

// 输出结果如下:
// --------------------------------------------------
// showA
// showB

// ========== 说明 ========== //
// 1、Go语言只有组合,没有继承。既然是组合,也就是说Teacher有两个ShowB()方法,不存在谁覆盖谁的问题。
// 2、这里也可以从另一个角度来理解,那就是Go语言里没有隐式转换,既然ShowA()方法声明了p的数据类型是*People,
//    那么p就肯定是*People类型变量,其中不可能发生隐式转换,从而p.ShowB()肯定是调用People.ShowB()方法。



🖥️ 常见语法题(16):下面代码会触发异常吗?请详细说明。

package main

import (
   "fmt"
   "runtime"
)

func main() {
   runtime.GOMAXPROCS(1)

   int_chan := make(chan int, 1)
   string_chan := make(chan string, 1)

   int_chan <- 1

   string_chan <- "hello"

   select {
   case value := <-int_chan:
      fmt.Println(value)
   case value := <-string_chan:
      panic(value)
   }
}

// ========== 说明 ========== //
// 1、程序可能触发异常,也可能不触发异常。
// 2、select{}块不是顺序执行里面的case,而是随机执行,并且只执行一个case。
//    如果执行第一个case就不会触发异常,如果执行第二个case就会触发异常。



🖥️ 常见语法题(17):下面代码输出什么?

package main

import "fmt"

func calc(index string, a, b int) int {
   ret := a + b
   fmt.Println(index, a, b, ret)
   return ret
}

func main() {
   a := 1
   b := 2

   defer calc("1", a, calc("10", a, b)) // 相当于 defer calc("1", 1, 3)

   a = 0

   defer calc("2", a, calc("20", a, b)) // 相当于 defer calc("2", 0, 2)

   b = 1
}

// 输出结果如下:
// --------------------------------------------------
// 10 1 2 3
// 20 0 2 2
// 2 0 2 2
// 1 1 3 4

// ========== 说明 ========== //
// 1、程序设置了两个defer,后设置的先执行这很容易理解,主要难点在defer执行calc()函数时第三个参数是calc()函数返回值,
//    其实在设置defer时传入的所有参数都是已经固定了的,也就是说程序在设置defer时就已经算出了第三个参数的最终值,
//    而不是在执行defer时才去计算第三个参数的值,明白了这一点那这道题就很简单了。
//    总结下来就是程序会调用4次calc(),按照调用顺序依次是:
//    calc("10", 1, 2)  --->  10 1 2 3  对应代码中的calc("10", a, b)
//    calc("20", 0, 2)  --->  20 0 2 2  对应代码中的calc("20", a, b)
//    calc("2", 0, 2)   --->  2 0 2 2   对应代码中的第二个defer
//    calc("1", 1, 3)   --->  1 1 3 4   对应代码中的第一个defer



🖥️ 常见语法题(18):请写出输出内容。

package main

import "fmt"

func main() {
   s := make([]int, 5)
   s = append(s, 1, 2, 3)
   fmt.Println(s) // [0 0 0 0 0 1 2 3]
}

// ========== 说明 ========== //
// 1、这个没什么好说的,切片s长度为5,但是没有指定初始值,所以5个元素的初始值都是int的零值,
//    然后又追加了三个数字,所以输出内容是“[0 0 0 0 0 1 2 3]”。



🖥️ 常见语法题(19):下面的代码有什么问题?

package main

import (
   "fmt"
   "sync"
)

type UserAges struct {
   ages map[string]int
   sync.Mutex
}

func (ua *UserAges) Add(name string, age int) {
   ua.Lock()
   defer ua.Unlock()
   ua.ages[name] = age
}

func (ua *UserAges) Get(name string) int {
   if age, ok := ua.ages[name]; ok {
      return age
   }
   return -1
}

func main() {
   ua := &UserAges{}
   ua.ages = map[string]int{}

   ua.Add("张三", 3)
   ua.Add("李四", 4)
   ua.Add("王五", 5)

   fmt.Println(ua.Get("张三")) // 3
   fmt.Println(ua.Get("李四")) // 4
   fmt.Println(ua.Get("王五")) // 5

   fmt.Println(ua.Get("赵六")) // -1
}

// ========== 说明 ========== //
// 1、在Go语言里map并不是协程安全的,开发者需要自己显式使用锁(sync.Mutex或sync.RWMutex)来解决数据竞争问题。
//    上面的代码虽然Add()方法加了锁,但Get()方法没有,所以应该给Get()方法也加锁。
// 2、题外话(1):可以把sync.Mutex改为sync.RWMutex,因为Get()方法是只读取数据,sync.RWMutex允许并发读效率更高。
//    题外话(2):对于高并发问题应该始终优先考虑使用管道来解决,而不是使用锁,除非遇到了连管道也无法解决的问题,
//               否则都应该使用管道。



🖥️ 常见语法题(20):下面的迭代会有什么问题?

package main

import (
   "fmt"
   "sync"
)

type threadSafeSet struct {
   sync.RWMutex
   s []interface{}
}

func (set *threadSafeSet) Iter() <-chan interface{} {
   ch := make(chan interface{})
   go func() {
      set.RLock()
      for elem := range set.s {
         ch <- elem
      }
      close(ch)
      set.RUnlock()
   }()
   return ch
}

func main() {
   th := threadSafeSet{
      s: []interface{}{"1", "2"},
   }
   v := <-th.Iter()
   fmt.Printf("%s%v", "ch", v)
}

// ========== 说明 ========== //
// 1、Iter()方法里的for-range无法迭代set.s,因为ch是容量为0的无缓冲管道,执行“ch <- elem”时会发生阻塞。
//    解决办法是声明ch变量是指定管道容量,为避免浪费可将容量设为len(set.s)。



🖥️ 常见语法题(21):以下代码能编译过去吗,为什么?

package main

import (
   "fmt"
)

type People interface {
   Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
   if think == "bitch" {
      talk = "You are a good boy"
   } else {
      talk = "hi"
   }
   return
}

func main() {
   var peo People = Student{}
   think := "bitch"
   fmt.Println(peo.Speak(think))
}

// ========== 说明 ========== //
// 1、上面的代码无法通过编译,出错的代码是“var peo People = Student{}”,错误信息如下:
//    cannot use Student{} (value of type Student) as People value in variable declaration: Student does not implement People (method Speak has pointer receiver)
//    翻译成中文就是,Student结构体没有实现People接口(Speak()方法有一个指针接收者)
//    从代码可以看到,实现Speak()方法的是*Student,而不是Student,所以“var peo People = Student{}”会报错,
//    所以只要把“var peo People = Student{}”改为“var peo People = &Student{}”即可。
//    或者把“func (stu *Student) Speak”改为“func (stu Student) Speak”也能通过编译,但不建议使用这种方式。



🖥️ 常见语法题(22):以下代码打印出来什么内容,说出为什么。

package main

import (
   "fmt"
)

type People interface {
   Show()
}

type Student struct{}

func (stu *Student) Show() {}

func live() People {
   var stu *Student
   return stu
}

func main() {
   if live() == nil {
      fmt.Println("AAAAAAA")
   } else {
      fmt.Println("BBBBBBB") // BBBBBBB
   }
}

// ========== 说明 ========== //
// 1、live()函数的返回值是People接口,将接口变量和nil进行判等务必非常小心,因为里面有非常多的坑。
//    首先要知道接口变量是包含类型和值两部分的,只有类型和值均为nil时该接口变量才等于nil。
//    我们来分析一下live()函数,它在里面声明了一个名为stu的*Student类型变量,由于没有赋值,所以stu的值为nil,
//    所以live()函数的返回值是一个类型为*Student、值为nil的People接口,由于类型不为nil,所以整个返回值也不等于nil。

Copyright © 2024 码农人生. All Rights Reserved