Go的优势
天生支持并发,性能高。
单一的标准代码格式,比其他语言更具可读性。
自动垃圾收集机制比Java和Python更有效,因为它与程序同时执行。
Go数据类型
go中的25个关键字
Go程序中的包是什么?
package packageName
声明import packageName
Go支持什么形式的类型转换?如何实现整数转为浮点数
go支持显示类型转换,即严格强制类型转换
a := 15
b := float64(a)
fmt.Println(b, reflect.TypeOf(b))
=
和 :=
的区别?
:= 声明+赋值= 仅赋值
var foo int
foo = 10
// 等价于
foo := 10
指针的作用?
指针用来保存变量的地址。
例如:
var x = 5
var p *int = &x
fmt.Printf("x = %d", *p) // x 可以用 *p 访问*
*运算符,也称为解引用运算符,用于访问地址中的值。
&运算符,也称为地址运算符,用于返回变量的地址。
Go 允许多个返回值吗?
允许
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("A", "B")
fmt.Println(a, b) // B A
}
Go 有异常类型吗?
Go 没有异常类型,只有错误类型(Error),通常使用返回值来表示异常状态。
f, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
什么是协程(Goroutine)?
如何高效地拼接字符串?
Go 语言中,字符串是只读的,也就意味着每次修改操作都会创建一个新的字符串。如果需要拼接多次,应使用 strings.Builder,最小化内存拷贝次数。
var str strings.Builder
for i := 0; i < 1000; i++ {
str.WriteString("a")
}
fmt.Println(str.String())
什么是 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
Go 支持默认参数或可选参数吗?
如何交换 2 个变量的值?
a, b := "A", "B"
a, b = b, a
fmt.Println(a, b) // B A
Go 语言 tag 的用处?
tag 可以理解为 struct 字段的注解,可以用来定义字段的一个或多个属性。框架/工具可以通过反射获取到某个字段定义的属性,采取相应的处理方式。tag 丰富了代码的语义,增强了灵活性。
例如:
package main
import "fmt"
import "encoding/json"
type Stu struct {
Name string `json:"stu_name"`
ID string `json:"stu_id"`
Age int `json:"-"`
}
func main() {
buf, _ := json.Marshal(Stu{
"Tom", "t001", 18})
fmt.Printf("%s\n", buf)
}
这个例子使用 tag 定义了结构体字段与 json 字段的转换关系,Name -> stu_name, ID -> stu_id,忽略 Age 字段。很方便地实现了 Go 结构体与不同规范的 json 文本之间的转换。
字符串打印时,%v
和 %+v
的区别
%v 和 %+v 都可以用来打印 struct 的值,区别在于 %v 仅打印各个字段的值,%+v 还会打印各个字段的名称。
type Stu struct {
Name string
}
func main() {
fmt.Printf("%v\n", Stu{
"Tom"}) // {Tom}
fmt.Printf("%+v\n", Stu{
"Tom"}) // {Name:Tom}
}
但如果结构体定义了 String() 方法,%v 和 %+v 都会调用 String() 覆盖默认值。
Go 语言中如何表示枚举值(enums)?
通常使用常量(const) 来表示枚举值。
type StuType int32
const (
Type1 StuType = iota
Type2
Type3
Type4
)
func main() {
fmt.Println(Type1, Type2, Type3, Type4) // 0, 1, 2, 3
}
参考 What is an idiomatic way of representing enums in Go? - StackOverflow
空 struct{} 的用途?
使用空结构体 struct{} 可以节省内存,一般作为占位符使用,表明这里并不需要一个值。
fmt.Println(unsafe.Sizeof(struct{
}{
})) // 0
比如使用 map 表示集合时,只关注 key,value 可以使用 struct{} 作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。
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{} 代替。
func main() {
ch := make(chan struct{
}, 1)
go func() {
<-ch
// do something
}()
ch <- struct{
}{
}
// ...
}
再比如,声明只包含方法的结构体。
type Lamp struct{
}
func (l Lamp) On() {
println("On")
}
func (l Lamp) Off() {
println("Off")
}
go中的cap函数可以作用于哪些内容?
可作用于的类型有:
查看他们的容量大小,而不是装的数据大小
go语言中new的作用是什么?
go语言中的make作用是什么?
总结make和new的区别?
如何在运行时检查变量类型?
Type Switch
)是在运行时检查变量类型的最佳方式。类型开关按类型而不是值来评估变量。每个 Switch
至少包含一个 case
用作条件语句
如果没有一个 case
为真,则执行 default
。
switch case fallthrough default使用场景
func main() {
var a int
for i := 0; i < 10; i++{
a = rand.Intn(100)
switch {
case a >= 80:
fmt.Println("优秀", a)
fallthrough // 强制执行下一个case
case a >= 60:
fmt.Println("及格", a)
fallthrough
default:
fmt.Println("不及格", a)
}
}
}
fmt包中Printf、Sprintf、Fprintf都是格式化输出,有什么不同?
虽然这三个函数都是格式化输出,但是输出的目标不一样
例如:
func main() {
var a int = 15
file, _ := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND, 0644)
// 格式化字符串并输出到文件
n, _ := fmt.Fprintf(file, "%T:%v:%p", a, a, &a)
fmt.Println(n)
}
go语言中的数组和切片的区别是什么?
go语言中值传递和地址传递(引用传递)如何运行?有什么区别?举例说明
go中的参数传递、引用传递
go语言中的所有的传参都是值传递(传值),都是一个副本,一个拷贝,
因为拷贝的内容有时候是非引用类型(int, string, struct)等,这样在函数中就无法修改原内容数据
有的是引用类型(指针、slice、map、chan),这样就可以修改原内容数据
go中的引用类型包含slice、map、chan,它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性
内置函数new计算类型大小,为其分配零值内存,返回指针。
而make会被编译器翻译成具体的创建函数,由其分配内存并初始化成员结构,返回对象而非指针
go中数组和切片在传递时有什么区别?
go中slice的底层实现
go中slice的扩容机制,有什么注意点?
go中是如何实现切片扩容的?[答案有误,需要重新确定]
当容量小于1024时,每次扩容容量翻倍,当容量大于1024时,每次扩容加25%.
func main() {
s1 := make([]int, 0)
for i := 0; i < 3000; i++{
fmt.Println("len =", len(s1), "cap = ", cap(s1))
s1 = append(s1, i)
}
}
扩容前后的slice是否相同?
如何判断 2 个字符串切片(slice) 是相等的?
go 语言中可以使用反射 reflect.DeepEqual(a, b) 判断 a、b 两个切片是否相等,但是通常不推荐这么做,使用反射非常影响性能。
通常采用的方式如下,遍历比较切片中的每一个元素(注意处理越界的情况)。
func StringSliceEqualBCE(a, b []string) bool {
if len(a) != len(b) {
return false
}
if (a == nil) != (b == nil) {
return false
}
b = b[:len(a)]
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
看下面代码defer的执行顺序是什么?defer的作用和特点是什么?
在普通函数或方法前加上defer关键字,就完成了defer所需要的语法,当defer语句被执行时,跟在defer语句后的函数会被延迟执行
知道包含该defer语句的函数执行完毕,defer语句后的函数才会执行,无论包含defer语句的函数是通过return正常结束,还是通过panic导致的异常结束
可以在一个函数中执行多条defer语句,由于在栈中存储,所以它的执行顺序和声明顺序相反
多个 defer 语句,遵从后进先出(Last In First Out,LIFO)的原则,最后声明的 defer 语句,最先得到执行。defer 在 return 语句之后执行,但在函数退出之前,defer 可以修改返回值。
例子:
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
这个例子中,可以看到 defer 的执行顺序:后进先出。但是返回值并没有被修改,这是由于 Go 的返回机制决定的,执行 return 语句后,Go 会创建一个临时变量保存返回值,因此,defer 语句修改了局部变量 i,并没有修改返回值。那如果是有名的返回值呢?
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,即对返回值产生了影响。
defer的常用场景
defer语句中通过recover捕获panic例子
注意要在defer后函数里的recover()
func main() {
defer func() {
err := recover()
fmt.Println(err)
}()
defer fmt.Println("first defer")
defer fmt.Println("second defer")
defer fmt.Println("third defer")
fmt.Println("哈哈哈哈")
panic("abc is an error")
}
哈希概念讲解
哈希表又称为散列表,由一个直接寻址表和一个哈希函数组成
由于哈希表的大小是有限的而要存储的数值是无限的,因此对于任何哈希函数,都会出现两个不同元素映射到相同位置的情况,这种情况叫做哈希冲突
通过拉链法解决哈希冲突:
哈希表的查找速度起决定性作用的就是哈希函数: 除法哈希发、乘法哈希法、全域哈希法
哈希表的应用?
go中的map底层实现
go中的map如何扩容
go中map的查找
如何判断 map 中是否包含某个 key ?
if val, ok := dict["foo"]; ok {
//do something here
}
dict[“foo”] 有 2 个返回值,val 和 ok,如果 ok 等于 true,则说明 dict 包含 key “foo”,val 将被赋予 “foo” 对应的值。
下面代码的输出是:
func main() {
const (
a, b = "golang", 100
d, e
f bool = true
g
)
fmt.Println(d, e, g)
}
答案:
golang 100 true
在同一个 const group 中,如果常量定义与前一行的定义一致,则可以省略类型和值。编译时,会按照前一行的定义自动补全。即等价于
gofunc main() {
const (
a, b = "golang", 100
d, e = "golang", 100
f bool = true
g bool = true
)
fmt.Println(d, e, g)
}
下面代码输出是:
func main() {
const N = 100
var x int = N
const M int32 = 100
var y int = M
fmt.Println(x, y)
}
答案:
编译失败:cannot use M (type int32) as type int in assignment
Go 语言中,常量分为无类型常量和有类型常量两种,const N = 100,属于无类型常量,赋值给其他变量时,如果字面量能够转换为对应类型的变量,则赋值成功,例如,var x int = N。但是对于有类型的常量 const M int32 = 100,赋值给其他变量时,需要类型匹配才能成功,所以显示地类型转换:
var y int = int(M)
下面代码的输出是:
func main() {
var a int8 = -1
var b int8 = -128 / a
fmt.Println(b)
}
答案:
-128
int8 能表示的数字的范围是 [-2^7, 2^7-1],即 [-128, 127]。-128 是无类型常量,转换为 int8,再除以变量 -1,结果为 128,常量除以变量,结果是一个变量。变量转换时允许溢出,符号位变为1,转为补码后恰好等于 -128。
对于有符号整型,最高位是是符号位,计算机用补码表示负数。补码 = 原码取反加一。
例如:-1 : 11111111
00000001(原码) 11111110(取反) 11111111(加一)
-128:
10000000(原码) 01111111(取反) 10000000(加一)
-1 + 1 = 011111111 + 00000001 = 00000000(最高位溢出省略)
-128 + 127 = -110000000 + 01111111 = 11111111
下面代码输出是:
func main() {
const a int8 = -1
var b int8 = -128 / a
fmt.Println(b)
}
答案:
编译失败:constant 128 overflows int8
-128 和 a 都是常量,在编译时求值,-128 / a = 128,两个常量相除,结果也是一个常量,常量类型转换时不允许溢出,因而编译失败。
下列代码输出是:
func main() {
var err error
if err == nil {
err := fmt.Errorf("err")
fmt.Println(1, err)
}
if err != nil {
fmt.Println(2, err)
}
}
答案:
1 err
:= 表示声明并赋值,= 表示仅赋值。
变量的作用域是大括号,因此在第一个 if 语句 if err == nil 内部重新声明且赋值了与外部变量同名的局部变量 err。对该局部变量的赋值不会影响到外部的 err。因此第二个 if 语句 if err != nil 不成立。所以只打印了 1 err。
下列代码输出:
type T struct{
}
func (t T) f(n int) T {
fmt.Print(n)
return t
}
func main() {
var t T
defer t.f(1).f(2)
fmt.Print(3)
}
答案:
132
defer 延迟调用时,需要保存函数指针和参数,因此链式调用的情况下,除了最后一个函数/方法外的函数/方法都会在调用时直接执行。也就是说 t.f(1) 直接执行,然后执行 fmt.Print(3),最后函数返回时再执行 .f(2),因此输出是 132。
func f(n int) {
defer fmt.Println(n)
n += 100
}
func main() {
f(1)
}
答案:
1
打印 1 而不是 101。defer 语句执行时,会将需要延迟调用的函数和参数保存起来,也就是说,执行到 defer 时,参数 n(此时等于1) 已经被保存了。因此后面对 n 的改动并不会影响延迟函数调用的结果。
func main() {
n := 1
defer func() {
fmt.Println(n)
}()
n += 100
}
答案:
101
匿名函数没有通过传参的方式将 n 传入,因此匿名函数内的 n 和函数外部的 n 是同一个,延迟执行时,已经被改变为 101。
func main() {
n := 1
if n == 1 {
defer fmt.Println(n)
n += 100
}
fmt.Println(n)
}
答案:
101
1
先打印 101,再打印 1。defer 的作用域是函数,而不是代码块,因此 if 语句退出时,defer 不会执行,而是等 101 打印后,整个函数返回时,才会执行。
go两个接口之间可以存在什么关系?
什么是 goroutine,你如何停止它?
goroutine是协程/轻量级线程/用户态线程,不同于传统的内核态线程
占用资源特别少,创建和销毁只在用户态执行不会到内核态,节省时间
创建goroutine需要使用go关键字
可以向goroutine发送一个信号通道来停止它,goroutine内部需要检查信号通道
例子:
func main() {
var wg sync.WaitGroup // 等待组进行多个任务的同步,可以保证并发环境中完成指定数量的任务,每个sync.WaitGroup值在内部维护着一个计数,此计数的初始默认值为0
var exit = make(chan bool)
wg.Add(1) // 等待组的计数器+1
go func() {
for {
select {
case <-exit: // 接收到信号后return退出当前goroutine
fmt.Println("goroutine接收到信号退出了!")
wg.Done() // 等待组的计数器-1
return
default:
fmt.Println("还没有接收到信号")
}
}
}()
exit <- true
wg.Wait() // 当等待组计数器不等于0时阻塞,直到变为0
}
go中同步锁(也叫互斥锁)有什么特点,作用是什么?何时使用互斥锁,何时使用读写锁?
当一个goroutine获得了Mutex(互斥锁)后,其它goroutine就只能乖乖等待,除非该goroutine释放Mutex
RWMutext(读写互斥锁)在读锁占用的情况下会阻止写,但不会阻止读,在写锁占用的情况下,会阻止任何其它goroutine进来
无论是读还是写,整个锁相当于由该goroutine独占
作用:保证资源在使用时的独有性,不会因为并发导致数据错乱,保证系统稳定性
案例:
package main
import (
"fmt"
"sync"
"time"
)
var (
num = 0
lock = sync.RWMutex{
} // 耗时:100+毫秒
//lock = sync.Mutex{} // 耗时:50+毫秒
)
func main() {
start := time.Now()
go func() {
for i := 0; i < 100000; i++{
lock.Lock()
//fmt.Println(num)
num++
lock.Unlock()
}
}()
for i := 0; i < 100000; i++{
lock.Lock()
//fmt.Println(num)
num++
lock.Unlock()
}
fmt.Println(num)
fmt.Println(time.Now().Sub(start))
}
总结:
goroutine案例(两个goroutine,一个负责输出数字,另一个负责输出26个英文字母,格式如下:12ab34cd56ef78gh … yz)
package main
import (
"fmt"
"sync"
"unicode/utf8"
)
// 案例:两个goroutine,一个负责输出数字,另一个负责输出26个英文字母,格式如下:12ab34cd56ef78gh ... yz
var (
wg = sync.WaitGroup{
} // 和第五题很相关。申明等待组
chNum = make(chan bool)
chAlpha = make(chan bool)
)
func main() {
go func() {
i := 1
for {
<-chNum // 接到信号,运行该goroutine
fmt.Printf("%v%v", i, i + 1)
i += 2
chAlpha <- true // 发送信号
}
}()
wg.Add(1) // 等待组的计数器+1
go func() {
str := "abcdefghigklmnopqrstuvwxyz"
i := 0
for {
<-chAlpha // 接到信号,运行该goroutine
fmt.Printf("%v", str[i:i+2])
i += 2
if i >= utf8.RuneCountInString(str){
wg.Done() // 等待组的计数器-1
return
}
chNum <- true // 发送信号
}
}()
chNum <- true // 发送信号
wg.Wait() // 等待组的计数器不为0时,阻塞main进程,直到等待组的计数器为0
}
介绍一下channel
go中channel的特性
channel中ring buffer的实现
go语言中,channel通道有什么特点,需要注意什么?
总结:
案例:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup // 等待组
var ch chan int // nil channel
var ch1 = make(chan int) // 创建channel
fmt.Println(ch, ch1) // <nil> 0xc000086060
wg.Add(1) // 等待组的计数器+1
go func() {
//ch <- 15 // 如果给一个nil的channel发送数据会造成永久阻塞
//<-ch // 如果从一个nil的channel中接收数据也会造成永久阻塞
ret := <-ch1
fmt.Println(ret)
ret = <-ch1 // 从一个已关闭的通道中接收数据,如果缓冲区中为空,则返回该类型的零值
fmt.Println(ret)
wg.Done() // 等待组的计数器-1
}()
go func() {
//close(ch1)
ch1 <- 15 // 给一个已关闭通道发送数据就会包panic错误
close(ch1)
}()
wg.Wait() // 等待组的计数器不为0时阻塞
}
go中channel缓冲有什么特点?
写一个定时任务,每秒执行一次
func main() {
t1 := time.NewTicker(time.Second * 1) // 创建一个周期定时器
var i = 1
for {
if i == 10{
break
}
select {
case <-t1.C: // 一秒执行一次的定时任务
task1(i)
i++
}
}
}
func task1(i int) {
fmt.Println("task1执行了---", i)
}
如何关闭 HTTP 的响应体的?
直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体;
手动调用 defer 来关闭响应体。
正确示例:
func main() {
resp, err := http.Get("http://www.baidu.com") // 发出请求并返回请求结果
// 关闭 resp.Body 的正确姿势
if resp != nil {
defer resp.Body.Close()
}
checkError(err) // 检查错误,省略写法
defer resp.Body.Close() // 手动调用defer来关闭响应体
body, err := ioutil.ReadAll(resp.Body) // 一次性读写文件的全部数据
checkError(err)
fmt.Println(string(body))
}
是否主动关闭过http连接,为啥要这样做?
有关闭,不关闭会程序可能会消耗完 socket 描述符。有如下2种关闭方式:
直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。
设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接
// 主动关闭连接
func main() {
req, err := http.NewRequest("GET", "http://golang.org", nil)
checkError(err)
req.Close = true // 直接设置请求变量的Close字段值为true,每次请求结束后主动关闭连接
//req.Header.Add("Connection", "close") // 等效的关闭方式
resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
你可以创建一个自定义配置的 HTTP transport(传输) 客户端,用来取消 HTTP 全局的复用连接。
func main() {
tr := http.Transport{
DisableKeepAlives: true} // 自定义配置传输客户端,用来取消HTTP全部的复用连接。
client := http.Client{
Transport: &tr}
resp, err := client.Get("https://golang.google.cn/")
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
fmt.Println(resp.StatusCode) // 200
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(len(string(body)))
}
解析 JSON 数据时,默认将数值当做哪种类型?
在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理。
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{
}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}
}
解析出来的 200 是 float 类型。
JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗?
首先 JSON 标准库对 nil slice 和 空 slice 的处理是不一致。
通常错误的用法,会报数组越界的错误,因为只是声明了slice,却没有给实例化的对象。
var slice []int // nil slice
slice[1] = 0
此时slice的值是nil,这种情况可以用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。
empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的,此时的定义如下:
slice := make([]int,0)// 空slice,没有值,空间也是空的
slice := []int{
}
当我们查询或者处理一个空的列表的时候,这非常有用,它会告诉我们返回的是一个列表,但是列表内没有任何值。总之,nil slice 和 empty slice是不同的东西,需要我们加以区分的。
go convey是什么,一般用来做什么?
说说go语言的beego框架
GoStub的作用是什么?
GoStub也是一种测试框架:
GoStub 可以对全局变量打桩
GoStub 可以对函数打桩
GoStub 不可以对类的成员方法打桩
GoStub 可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为
整理不易,给个赞吧!~~~
更多【编程技术-Go基础面经大全(持续补充中)】相关视频教程:www.yxfzedu.com