1.無論sync.Mutex還是其衍生品都會提示不能複製,但是能夠編譯運行
加鎖後複製變量,會將鎖的狀態也複製,所以 mu1 其實是已經加鎖狀態,再加鎖會死鎖.
所以此題的答案是 fatal error;
type MyMutex struct {
count int
sync.Mutex
}
func main() {
var mu MyMutex
mu.Lock()
var mu1 = mu
mu.count++
mu.Unlock()
mu1.Lock()
mu1.count++
mu1.Unlock()
fmt.Println(mu.count, mu1.count)
}
2.defer對於有返回變量的函數,它的生命其可以保留到return之後.
func main() {
fmt.Println(doubleScore(0)) //0
fmt.Println(doubleScore(20.0)) //40
fmt.Println(doubleScore(50.0)) //100
}
func doubleScore(source float32) (score float32) {
defer func() {
if score < 1 || score >= 100 {
score = source //最終還能修改返回值
}
}()
return source * 2
}
3.sync.Mutex不能重複加鎖
var mu sync.Mutex
var chain string
func main() {
chain = "main"
A()
fmt.Println(chain)
}
func A() {
mu.Lock()
defer mu.Unlock()
chain = chain + " --> A"
B()
}
func B() {
chain = chain + " --> B"
C()
}
func C() {
mu.Lock() //A處還沒有UnLock,此處又加鎖,所以出現deadlock
defer mu.Unlock()
chain = chain + " --> C"
}
3.go的for語句幾大特別之處
a.break到指定的層級
func main() {
OuterLoop:
for i := 0; i < 2; i++ {
InLoop:
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
break InLoop
case 3:
fmt.Println(i, j)
break OuterLoop
}
}
}
}
b.類似於while
for true{
//do sth.
}
for{
//do sth.
}
c.三分號的用法和c++類似
for ; step > 0; step-- {
fmt.Println(step)
}
for ; ; i++ {
if i > 10 {
break
}
}
func main() {
v := []int{1, 2, 3}
for i, n := 0, len(v); i < n; i++ {
v = append(v, i)
}
fmt.Println(v)
}
4.go的一些預定義字符
這些不是關鍵字,但最好不要拿去用,面試出這樣的題的話,意義不大,只能拿來搞怪.
true /false
append 函數
append 函數用於向切片中添加元素,並返回新的切片。
make 函數
make 函數用於創建切片、映射和通道。
new 函數
new 函數用於分配內存並返回指向新分配的零值對象的指針。
len 函數
len 函數用於返回字符串、切片、映射、通道、數組等的長度。
cap 函數
cap 函數用於返回切片、數組、通道等的容量。
copy 函數
copy 函數用於複製切片中的元素。
delete 函數
delete 函數用於從映射中刪除指定的鍵值對。
print 和 println 函數
print 和 println 函數用於打印輸出信息。
panic 和 recover 函數
panic 函數用於引發運行時錯誤,recover 函數用於捕獲並處理運行時錯誤。
close 函數
close 函數用於關閉通道。
5.有參返回類型可以直接return
以下等價,但是return的作用域內(代碼塊內)必須有ret變量的存在,
下圖中ret被新的ret隱藏了,導致return所在作用域丟失ret.所以無法編譯.
func add1(a, b int) (ret int) {
ret = a + b
return
}
func add2(a, b int) (ret int) {
ret = a + b
return ret
}
6.json.Unmarshal必須傳入目標指針
雖然map的根本就是hmap指針,但是 reflect.ValueOf(v).Kind()獲取到的不是指針,所以也必須傳入指針.
func (d *decodeState) unmarshal(v any) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Pointer || rv.IsNil() {//!!
return &InvalidUnmarshalError{reflect.TypeOf(v)}
}
d.scan.reset()
d.scanWhile(scanSkipSpace).
err := d.value(rv)
if err != nil {
return d.addErrorContext(err)
}
return d.savedError
}
7.slice因爲切片底層公用數組,容易導致數據共享和不能內存不能及時釋放
func get() []byte {
raw := make([]byte, 10000)
fmt.Println(len(raw), cap(raw), &raw[0])
return raw[:3]
}
func main() {
data := get()
fmt.Println(len(data), cap(data), &data[0])
}
8.go 語言中的可比較類型和不可比較類型
注意不可比較類型就三個:map slice func,可nil類型還包括chan,指針
但是chan,指針可以用於等值比較
操作符 | 變量類型 |
---|---|
等值操作符 (==、!=) | 整型、浮點型、字符串、布爾型、複數、 指針、管道、接口、結構體、數組 |
排序操作符 (<、<=、> 、 >=) | 整型、浮點型、字符串 |
不可比較類型 | map、slice、function |
所以下面代碼結果爲false,同一類型指針是可以進行相等比較的
type foo struct{ Val int }
type bar struct{ Val int }
func main() {
a := &foo{Val: 5}
b := &foo{Val: 5}
fmt.Print(a == b)
}
9.go每個case上的表達式都會執行,但只有一個chan可以讀取或設置值
https://xie.infoq.cn/article/49526fb0dde758d663dfe0cd0 完整地介紹chan使用
func A() int {
fmt.Println("A", GoID())
time.Sleep(500 * time.Millisecond)
return 1
}
func B() int {
fmt.Println("B", GoID())
time.Sleep(1000 * time.Millisecond)
return 2
}
func GoID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
func main() {
ch := make(chan int, 1)
go func() {
select {
case ch <- A():
{
fmt.Println("caseA")
}
case ch <- B():
{
fmt.Println("caseB")
}
default:
ch <- 3
}
}()
for v := range ch {
fmt.Println(v)
}
}
10.基礎類型的map可以++,非基礎類型不行.
這種差異造成詭異的心智負擔.
func main() {
m := make(map[string]int)
m["foo"]++ //基礎類型沒有問題
fmt.Println(m["foo"])
m2 := map[string]Person{}
m2["foo"] = Person{1}
m2["foo"].Age = 22 //非法操作
}
type Person struct {
Age int
}
11.一個接口是否==nil,要看其類型和數值是否同時爲nil
打印則看數值
func Foo() error {
var err *os.PathError = nil
return err
}
func main() {
err := Foo()
fmt.Println(err) //nil,打印則看數值
fmt.Println(err == nil) //false
}
func main() {
x := interface{}(nil)
y := (*int)(nil)
a := y == x //類型不一樣
b := y == nil //y不是接口,所以直接看值.如果var y any = (*int)(nil)就變成接口了.最後接口就是false.
_, c := x.(interface{}) //沒有類型,斷言失敗
println(a, b, c) //flase true false
}
12.在go裏面0開始的數字是八進制
const (
Decade = 010
)
func main() {
fmt.Println(Decade) //8
}
13.字符串Trim操作注意
Trim結尾處的字符串,對應其他語言的TrimEnd
TrimSuffix
切勿使用TrimRight,會將第二個參數字符串裏面所有的字符拿出來處理,只要與其中任何一個字符相等,便會將其刪除
fmt.Println(strings.TrimRight("ABBA", "BA")) //最後全部被刪了
Trim以某段開始的字符串,對應其他語言的TrimStart
TrimPrefix
fmt.Println(strings.TrimLeft("ABBAC", "BA")) //最後只會剩下C
14.關於for range
如果是chan則,沒有k,只有v;
for v := range ch {
fmt.Println(v)
}
15.關於切片兩個冒號的說明
https://segmentfault.com/a/1190000018356577
注意
我們潛意識覺得cap就是底層數組的長度,但是在盡顯冒號切片時,cap的長度是有max-low得到的,max的默認值爲源切片(或源數組)的cap.
通過兩個冒號創建切片,slice[x:y:z]切片實體[x:y]切片長度len = y-x,切片容量cap = z-x\
data := [...]int{0, 1, 2, 3, 4, 5, 6} //初始化一個數組
slice := data[1:4:5] // [low : high : max] 通過兩個冒號創建切片
使用兩個冒號[1:4:5] 從數組中創建切片,長度爲4-1=3,也就是索引從1到3 的數據(1,2,3)
然後,後面是最大是5,即容量是5-1=4,即,創建的切片是長度爲從索引爲 1、2、3 的切片,底層數組爲[ 1,2,3,4]
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
data[:6:8] [0 1 2 3 4 5] //默認low爲0
data[5:] [5 6 7 8 9] //省略 high、 max,默認值爲源切片(或源數組)的cap
data[:3] [0 1 2] //low=0,max=默認值爲源切片(或源數組)的cap
data[:] [0 1 2 3 4 5 6 7 8 9] //10 10 全部省略。
一道題懂了,那真懂了.
func main() {
a := []int{0, 1, 2, 3}
s := a[1:3] //1,2,[3]
s2 := s[1:2] //2,[3]
fmt.Println(cap(s), len(s))
fmt.Println(cap(s2), len(s2))
fmt.Println("--------------------------------")
s2 = append(s2, 333)
fmt.Println(a)
fmt.Println(s)
fmt.Println(s2)
}
3 2
2 1
--------------------------------
[0 1 2 333]
[1 2]
[2 333]
16.關於切片的一些手法
創建空切片
// 使用 make 創建空的整型切片
slice := make([]int, 0)
// 使用切片字面量創建空的整型切片
slice := []int{}
17.for range 常數
for i := range 10 {
fmt.Println(i)
}
18.關於一切等值運算的根本是比較補碼
func main() {
count := 0
for i := range [256]struct{}{} {
m, n := byte(i), int8(i)
if n == -n {
count++
fmt.Println("n==-n", n, -n, i)
}
if m == -m {
count++
fmt.Println("m==-m", m, -m, i)
}
}
fmt.Println(count)
}
n==-n 0 0 0
m==-m 0 0 0
n==-n -128 -128 128
m==-m 128 128 128
4
-128 的補碼:
首先,我們需要知道 -128 的原碼。原碼是表示數值的二進制形式,其中最高位表示符號位(0 表示正數,1 表示負數),其餘位表示數值部分。對於 -128,其原碼爲 1000 0000。
接下來,我們求出 -128 的反碼。反碼是將原碼的除符號位外的各位取反。對於 -128,其反碼爲 1111 1111。
最後,我們計算 -128 的補碼。補碼是在反碼的末位加1,從而使後7位再次發生溢出,進位丟棄,符號位不變。因此,-128 的補碼爲 1000 0000。
需要注意的是,-128 是一個特殊的數,因爲它的絕對值比最小的32位整數還要大1,所以在計算機中表示爲 -128 的補碼時,我們直接使用其反碼加1的結果。
128的補碼
128 的原碼是 10000000。三碼都是這個,所以也是 10000000。
19.多重賦值的優先級
多重賦值分爲兩個步驟,有先後順序:
- 計算等號左邊的索引表達式和取址表達式,接着計算等號右邊的表達式;
- 賦值;
func main() {
var a []int = nil
a, a[0] = []int{1, 2}, 9
fmt.Println(a)
}
解析:運行時錯誤。知識點:多重賦值。
19.sync.WaitGroup不能複製值,且需要在啓動協程前Add
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
go func(wg sync.WaitGroup, i int) {
wg.Add(1)
fmt.Printf("i:%d\n", i)
wg.Done()
}(wg, i)
}
wg.Wait()
fmt.Println("exit")
}
20.chan的基本數據結構
ype hchan struct {
qcount uint // 隊列中的總元素個數
dataqsiz uint // 環形隊列大小,即可存放元素的個數
buf unsafe.Pointer // 環形隊列指針
elemsize uint16 //每個元素的大小
closed uint32 //標識關閉狀態
elemtype *_type // 元素類型
sendx uint // 發送索引,元素寫入時存放到隊列中的位置
recvx uint // 接收索引,元素從隊列的該位置讀出
recvq waitq // 等待讀消息的goroutine隊列
sendq waitq // 等待寫消息的goroutine隊列
lock mutex //互斥鎖,chan不允許併發讀寫
}
21.cap 關於 cap 函數適用下面哪些類型?
A. 數組;
B. channel;
C. map;
D. slice;
func main() {
c := make(chan int, 2)
c <- 11
fmt.Println(len(c)) //1
fmt.Println(cap(c)) //2
}
22.啓動一個goroutine的前提是,還有數
func main() {
runtime.GOMAXPROCS(1)
go func() {
for i:=0;i<10 ;i++ {
fmt.Println(i)
}
}()
for {}
}
23.接收器方法在defer時也會優先求值
type Slice []int
func NewSlice() Slice {
return make(Slice, 0)
}
func (s *Slice) Add(elem int) *Slice {
*s = append(*s, elem) //!! 注意無名類型的特別之處.
fmt.Print(elem)
return s
}
func main() {
s := NewSlice()
defer s.Add(1).Add(2)
s.Add(3)
}
//1 3 2
24.閉包引用相同變量,那麼他們的作用效果都在這個變量上
func test(x int) (func(), func()) {
return func() {
println(x)
x += 10
}, func() {
println(x)
}
}
func main() {
a, b := test(100)
a()
b()
}
// 100 110
25.range相當於一個函數,會對傳入的數據進行拷貝.
因此,下面切片和數組的場景會產生不同的值
type T struct {
n int
}
func main() {
ts := [2]T{}
//ts := make([]T, 2)
for i, t := range ts {
switch i {
case 0:
t.n = 3
ts[1].n = 9 //改變了ts,但t的數據,源頭是range產生的拷貝
case 1:
fmt.Print(t.n, " ")
}
}
fmt.Print(ts)
}
// 數組 0 [{0} {9}]
// 切片 9 [{0} {9}]
//--------------
func main() {
var a = []int{1, 2, 3, 4, 5}
var r = make([]int, 0)
for i, v := range a { //這裏產生了新的副本,所以下面的添加操作不影響遍歷.
if i == 0 {
a = append(a, 6, 7)
}
r = append(r, v)
}
fmt.Println(r)
}
25.關於defer func recover panic
- panic會將恐慌壓入一個後進先出的棧裏面;
- recover 必須內置於defer func 內纔有效;
- recover從panic棧中取值;
func main() {
defer func() {
fmt.Print(recover())
}()
defer func() {
defer func() {
fmt.Print(recover())
}()
panic(1)
}()
defer recover() //這裏的recover沒有效果,因爲沒有在defer func內部
panic(2)
}
上面的代碼panic依次入站 2 ,1,出棧爲1 ,2,所以結果 1 2
func main() {
defer func() {
fmt.Print(recover())
}()
defer func() {
defer fmt.Print(recover()) //首先參數需要求值,所以在當前執行func完畢時要打印2
panic(1) //壓入棧中,當前有沒有機會了,上級會捕捉到.
}()
defer recover() //這裏的recover沒有效果,因爲沒有在defer func內部
panic(2)
}
結果爲 2 1
26. 當閉包含有外層變量時,會讓外面的變量一直活着
func F(n int) func() int {
return func() int {
n++
return n
}
}
func main() {
f := F(5) //參數爲5
defer func() {
fmt.Println(f())
}()
defer fmt.Println(f()) //需要立即求值,所以最後打印6,參數變成爲了6
i := f() //參數變成了7,
fmt.Println(i)//打印7
}
//最後結果768
27.關於for,range對枚舉器的賦值
func main() {
var k = 9
for k = range []int{} { //沒有數據,所以對k的賦值,沒能執行
}
fmt.Println(k)
for k = 0; k < 3; k++ {
}
fmt.Println(k) //很明顯上面導致k變成了3
for k = range (*[3]int)(nil) { //一個[三個數據的數組]的指針,雖然是nil,但是在go裏面這是合法的,最終k變成2
}
fmt.Println(k)
}
28.關於繼承的本質問題
type T struct{}
func (*T) foo() {
}
func (T) bar() {
}
type S struct {
*T //注意指針也可以,也即,沒有名稱只有類型,那麼我們就可以通過語法糖的方式,好像調用自己的方法一樣調用
}
func main() {
s := S{}
_ = s.foo
s.foo() //這個是沒有問題的.雖然就nil,但因爲沒有發生調用nil,所以沒有問題
_ = s.bar // (* s.T).bar,但指針s.T爲nil,解引用失敗報錯
}
//---------------這是可以的
type X struct {}
func (x *X) test() {
println(x)
}
func main() {
var a *X
a.test()//nil,不是null
X{}.test() //這裏有問題,右值不可尋址
}
//--------------------------------------
type Father struct{ Name string }
type Monther struct {
Name string
}
func (m Monther) World() {
}
type Child struct {
Father
Monther
}
func (entity Father) Hello() {
}
func (entity Father) World() {
}
func main() {
child := Child{}
child.World() //二義性語法糖就用不了,需要指明Father or Monther的方法
}
//-----------------------
- 匿名字段的本質字段名是
S *S
S S
- 然後調用他們對應的接收器方法。
- 如果T爲指針,則全有,不問上面的兩種情況了
- 能不能改變原有數據,取決於接收器是不是指針類型
func main() {
var v T
t := &v
fmt.Printf("%p\n", t.S)
t.SPtr(1) //因爲t指針類型,所以擁有S,*S的接收器方法
t.SPtr(1)
fmt.Println(t.S.Age)
}
type S struct {
Age int
}
type T struct {
S
}
func (s S) SVal(dd int) {
s.Age = 10
fmt.Printf("%p\n", &s)
}
func (s *S) SPtr(dd int) {
fmt.Printf("%p\n", s)
s.Age = 100
}
func (t T) TVal(dd int) {
}
func (t *T) TPtr(dd int) {
}
func methodSet(arg interface{}) {
argType := reflect.TypeOf(arg)
fmt.Println(argType.NumMethod())
for i := 0; i < argType.NumMethod(); i++ {
m := argType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}
//----------
type Fragment interface {
Exec(transInfo *TransInfo) error
}
type GetPodAction struct {
}
func (g GetPodAction) Exec(transInfo *TransInfo) error {
...
return nil
}
指針接收器擁有值接口器的方法
var fragment Fragment = new(GetPodAction)
####29.關於map的怪異之處,面試時尤其注意
func main() {
var m map[int]bool // nil
var a = m[123]
fmt.Println(a)//false,原因很簡單,nil不是null,map不僅允許讀取沒有的key,還允許讀取nil的map
}
30.關於切片
- 關於切片之前的理解是有問題的,我們切片的對象是底層真實的數組長度(即:容量)
- low,high,max high默認是slice的長度,max則爲容量
func main() {
x := make([]int, 2, 10)
a := x[6:10]
b := x[6:] //x[6:2],所以這裏要報錯
c := x[2:] //2-2=0,所以最後是空的
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
31.不可比較類型 slice map func
func main() {
var x interface{}
var y interface{} = []int{3, 5}
_ = x == x
_ = y == x //注意雖然slice是不可以比較的類型,但是它可以和any nil比較
println("end")
_ = y == y //2 slice是不可以比較的
}
32.對於接收器方法的表達,我們都很陌生
type N int
func (n N) test() {
fmt.Println(n)
}
func main() {
var n N = 10
fmt.Println(n)
n++
//func (N).test()
f1 := N.test
f1(n)
n++
//var f2 func(*N)
f2 := (*N).test
f2(&n)
}
33.讀寫nil類型的chan都會永遠阻塞
func main() {
var ch chan int
select {
case v, ok := <-ch:
println(v, ok)
default:
println("default")
}
}
34.:=操作符不能給結構體字段賦值
type foo struct {
bar int
}
func main() {
var f foo
f.bar, tmp := 1, 2
}
35.go中所有的變量申明瞭就必須用,但常量除外,常量不能取地址
func main() {
const x = 123
const y = 1.23
fmt.Println(x)
}
36.byte在go中是uint8的別名,他們完全等價
type byte = uint8
func test(x byte) {
fmt.Println(x)
}
func main() {
var a byte = 0x11
var b uint8 = a
var c uint8 = a + b
test(c)
}
37.關於給切片加索引賦值的注意項
- 字面量初始化切片時候,可以指定索引,沒有指定索引的元素,其索引=前一個索引+1,
- 空缺的索引的位置,數據就是零值
var x = []int{2: 2, 3, 0: 1}
func main() {
fmt.Println(x) // [1 0 2 3]
}
38.關於select
A. select機制用來處理異步IO問題;//輸入輸出,要讀取或寫入chan
B. select機制最大的一條限制就是每個case語句裏必須是一個IO操作;
C. golang在語言級別支持select關鍵字;
39.指針類型的map和slice均不能使用索引
40.chan的寫入方負責關閉,不然造成泄露
func main() {
ch := make(chan int, 100)
// A
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}()
// B
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 * 10)
}
41. chan的幾個特性
A. 給一個 nil channel 發送數據,造成永遠阻塞
B. 從一個 nil channel 接收數據,造成永遠阻塞
C. 給一個已經關閉的 channel 發送數據,引起 panic
D. 從一個已經關閉的 channel 接收數據,如果緩衝區中爲空,則返回一個零值
42.類型轉換的方式和c繫有差異需要注意
B.
type MyInt int
var i int = 1
var j MyInt = (MyInt)i //c系的方式,在go裏面是非法的
C.
type MyInt int
var i int = 1
var j MyInt = MyInt(i)//在纔是正確的方式.
43.關於for range枚舉器,不同版本有不同定義
type Foo struct {
bar string
}
func main() {
s1 := []Foo{
{"A"},
{"B"},
{"C"},
}
s2 := make([]*Foo, len(s1))
for i, value := range s1 {
s2[i] = &value
}
fmt.Println(s1[0], s1[1], s1[2])
fmt.Println(s2[0], s2[1], s2[2])
}
1.22輸出:
{A} {B} {C}
&{A} &{B} &{C}
1.22之前
s2 的輸出是 &{C} &{C} &{C}
43.關於defer nil方法
func f(n int) (r int) {
defer func() {
r += n
recover() //這裏捕捉了nil方法的調用
}()
var f func()
defer f() //f是一個nil,所以沒有機會執行下面的方法
f = func() {
fmt.Println("f called")
r += 2
}
return n + 1
}
func main() {
fmt.Println(f(3)) //7
}