Golang 需要避免踩的 50 個坑(二)

2021-02-25 aoho求索

最近準備寫一些關於golang的技術博文,本文是之前在GitHub上看到的golang技術譯文,感覺很有幫助,先給各位讀者分享一下。

前言

Go 是一門簡單有趣的程式語言,與其他語言一樣,在使用時不免會遇到很多坑,不過它們大多不是 Go 本身的設計缺陷。如果你剛從其他語言轉到 Go,那這篇文章裡的坑多半會踩到。

如果花時間學習官方 doc、wiki、討論郵件列表、 Rob Pike 的大量文章以及 Go 的源碼,會發現這篇文章中的坑是很常見的,新手跳過這些坑,能減少大量調試代碼的時間。

初級篇:1-35(二)18. string 與索引操作符

對字符串用索引訪問返回的不是字符,而是一個 byte 值。

這種處理方式和其他語言一樣,比如 PHP 中:

1> php -r '$name="中文"; var_dump($name);'    
2string(6) "中文"
3
4> php -r '$name="中文"; var_dump($name[0]);' 
5string(1) "�"    
6
7> php -r '$name="中文"; var_dump($name[0].$name[1].$name[2]);'
8string(3) "中"

1func main() {
2    x := "ascii"
3    fmt.Println(x[0])       
4    fmt.Printf("%T\n", x[0])
5}

如果需要使用 for range 迭代訪問字符串中的字符(unicode code point / rune),標準庫中有 "unicode/utf8" 包來做 UTF8 的相關解碼編碼。另外 utf8string 也有像 func (s *String) At(i int) rune 等很方便的庫函數。

19. 字符串並不都是 UTF8 文本

string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值時才是 UTF8 文本,字串可以通過轉義來包含其他數據。

判斷字符串是否是 UTF8 文本,可使用 "unicode/utf8" 包中的 ValidString() 函數:

1func main() {
2    str1 := "ABC"
3    fmt.Println(utf8.ValidString(str1)) 
4
5    str2 := "A\xfeC"
6    fmt.Println(utf8.ValidString(str2)) 
7
8    str3 := "A\\xfeC"
9    fmt.Println(utf8.ValidString(str3)) 
10}

20. 字符串的長度

在 Python 中:

1data = u'♥'  
2print(len(data)) 

然而在 Go 中:

1func main() {
2    char := "♥"
3    fmt.Println(len(char))  
4}

Go 的內建函數 len() 返回的是字符串的 byte 數量,而不是像 Python 中那樣是計算 Unicode 字符數。

如果要得到字符串的字符數,可使用 "unicode/utf8" 包中的 RuneCountInString(str string) (n int)

1func main() {
2    char := "♥"
3    fmt.Println(utf8.RuneCountInString(char))   
4}

注意: RuneCountInString 並不總是返回我們看到的字符數,因為有的字符會佔用 2 個 rune:

1func main() {
2    char := "é"
3    fmt.Println(len(char))  
4    fmt.Println(utf8.RuneCountInString(char))   
5    fmt.Println("cafe\u0301")   
6}

參考:normalization

21. 在多行 array、slice、map 語句中缺少 `,` 號

1func main() {
2    x := []int {
3        1,
4        2   
5    }
6    y := []int{1,2,}    
7    z := []int{1,2} 
8    
9}

聲明語句中 } 摺疊到單行後,尾部的 , 不是必需的。

22. `log.Fatal` 和 `log.Panic` 不只是 log

log 標準庫提供了不同的日誌記錄等級,與其他語言的日誌庫不同,Go 的 log 包在調用 Fatal*()、Panic*() 時能做更多日誌外的事,如中斷程序的執行等:

1func main() {
2    log.Fatal("Fatal level log: log entry")     
3    log.Println("Nomal level log: log entry")
4}

23. 對內建數據結構的操作並不是同步的

儘管 Go 本身有大量的特性來支持並發,但並不保證並發的數據安全,用戶需自己保證變量等數據以原子操作更新。

goroutine 和 channel 是進行原子操作的好方法,或使用 "sync" 包中的鎖。

24. range 迭代 string 得到的值

range 得到的索引是字符值(Unicode point / rune)第一個字節的位置,與其他程式語言不同,這個索引並不直接是字符在字符串中的位置。

注意一個字符可能佔多個 rune,比如法文單詞 café 中的 é。操作特殊字符可使用norm 包。

for range 迭代會嘗試將 string 翻譯為 UTF8 文本,對任何無效的碼點都直接使用 0XFFFD rune(�)UNicode 替代字符來表示。如果 string 中有任何非 UTF8 的數據,應將 string 保存為 byte slice 再進行操作。

1func main() {
2    data := "A\xfe\x02\xff\x04"
3    for _, v := range data {
4        fmt.Printf("%#x ", v)   
5    }
6
7    for _, v := range []byte(data) {
8        fmt.Printf("%#x ", v)   
9    }
10}

25. range 迭代 map

如果你希望以特定的順序(如按 key 排序)來迭代 map,要注意每次迭代都可能產生不一樣的結果。

Go 的運行時是有意打亂迭代順序的,所以你得到的迭代結果可能不一致。但也並不總會打亂,得到連續相同的 5 個迭代結果也是可能的,如:

1func main() {
2    m := map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}
3    for k, v := range m {
4        fmt.Println(k, v)
5    }
6}

如果你去 Go Playground 重複運行上邊的代碼,輸出是不會變的,只有你更新代碼它才會重新編譯。重新編譯後迭代順序是被打亂的:

26. switch 中的 fallthrough 語句

switch 語句中的 case 代碼塊會默認帶上 break,但可以使用 fallthrough 來強制執行下一個 case 代碼塊。

1func main() {
2    isSpace := func(char byte) bool {
3        switch char {
4        case ' ':   
5        
6        case '\t':
7            return true
8        }
9        return false
10    }
11    fmt.Println(isSpace('\t'))  
12    fmt.Println(isSpace(' '))   
13}

不過你可以在 case 代碼塊末尾使用 fallthrough,強制執行下一個 case 代碼塊。

也可以改寫 case 為多條件判斷:

1func main() {
2    isSpace := func(char byte) bool {
3        switch char {
4        case ' ', '\t':
5            return true
6        }
7        return false
8    }
9    fmt.Println(isSpace('\t'))  
10    fmt.Println(isSpace(' '))   
11}

27. 自增和自減運算

很多程式語言都自帶前置後置的 ++、-- 運算。但 Go 特立獨行,去掉了前置操作,同時 ++、— 只作為運算符而非表達式。

1
2func main() {
3    data := []int{1, 2, 3}
4    i := 0
5    ++i         
6    fmt.Println(data[i++])  
7}
8
9
10
11func main() {
12    data := []int{1, 2, 3}
13    i := 0
14    i++
15    fmt.Println(data[i])    
16}

28. 按位取反

很多程式語言使用 ~ 作為一元按位取反(NOT)操作符,Go 重用 ^ XOR 操作符來按位取反:

1
2func main() {
3    fmt.Println(~2)     
4}
5
6
7
8func main() {
9    var d uint8 = 2
10    fmt.Printf("%08b\n", d)     
11    fmt.Printf("%08b\n", ^d)    
12}

同時 ^ 也是按位異或(XOR)操作符。

一個操作符能重用兩次,是因為一元的 NOT 操作 NOT 0x02,與二元的 XOR 操作 0x22 XOR 0xff 是一致的。

Go 也有特殊的操作符 AND NOT &^ 操作符,不同位才取1。

1func main() {
2    var a uint8 = 0x82
3    var b uint8 = 0x02
4    fmt.Printf("%08b [A]\n", a)
5    fmt.Printf("%08b [B]\n", b)
6
7    fmt.Printf("%08b (NOT B)\n", ^b)
8    fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n", b, 0xff, b^0xff)
9
10    fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n", a, b, a^b)
11    fmt.Printf("%08b & %08b = %08b [A AND B]\n", a, b, a&b)
12    fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n", a, b, a&^b)
13    fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n", a, b, a&(^b))
14}

110000010 [A]
200000010 [B]
311111101 (NOT B)
400000010 ^ 11111111 = 11111101 [B XOR 0xff]
510000010 ^ 00000010 = 10000000 [A XOR B]
610000010 & 00000010 = 00000010 [A AND B]
710000010 &^00000010 = 10000000 [A 'AND NOT' B]
810000010&(^00000010)= 10000000 [A AND (NOT B)]

29. 運算符的優先級

除了位清除(bit clear)操作符,Go 也有很多和其他語言一樣的位操作符,但優先級另當別論。

1func main() {
2    fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n", 0x2&0x2+0x4) 
3    
4    
5    
6
7    fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n", 0x2+0x2<<0x1)   
8    
9    
10    
11
12    fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n", 0xf|0x2^0x2) 
13    
14    
15    
16}

優先級列表:

1Precedence    Operator
2    5             *  /  %  <<  >>  &  &^
3    4             +  -  |  ^
4    3             ==  !=  <  <=  >  >=
5    2             &&
6    1             ||

30. 不導出的 struct 欄位無法被 encode

以小寫字母開頭的欄位成員是無法被外部直接訪問的,所以 struct 在進行 json、xml、gob 等格式的 encode 操作時,這些私有欄位會被忽略,導出時得到零值:

1func main() {
2    in := MyData{1, "two"}
3    fmt.Printf("%#v\n", in) 
4
5    encoded, _ := json.Marshal(in)
6    fmt.Println(string(encoded))    
7
8    var out MyData
9    json.Unmarshal(encoded, &out)
10    fmt.Printf("%#v\n", out)    
11}

31. 程序退出時還有 goroutine 在執行

程序默認不等所有 goroutine 都執行完才退出,這點需要特別注意:

1
2func main() {
3    workerCount := 2
4    for i := 0; i < workerCount; i++ {
5        go doIt(i)
6    }
7    time.Sleep(1 * time.Second)
8    fmt.Println("all done!")
9}
10
11func doIt(workerID int) {
12    fmt.Printf("[%v] is running\n", workerID)
13    time.Sleep(3 * time.Second)     
14    fmt.Printf("[%v] is done\n", workerID)
15}

如下,main() 主程序不等兩個 goroutine 執行完就直接退出了:

常用解決辦法:使用 "WaitGroup" 變量,它會讓主程序等待所有 goroutine 執行完畢再退出。

如果你的 goroutine 要做消息的循環處理等耗時操作,可以向它們發送一條 kill 消息來關閉它們。或直接關閉一個它們都等待接收數據的 channel:

1
2
3func main() {
4    var wg sync.WaitGroup
5    done := make(chan struct{})
6
7    workerCount := 2
8    for i := 0; i < workerCount; i++ {
9        wg.Add(1)
10        go doIt(i, done, wg)
11    }
12
13    close(done)
14    wg.Wait()
15    fmt.Println("all done!")
16}
17
18func doIt(workerID int, done <-chan struct{}, wg sync.WaitGroup) {
19    fmt.Printf("[%v] is running\n", workerID)
20    defer wg.Done()
21    <-done
22    fmt.Printf("[%v] is done\n", workerID)
23}

執行結果:

看起來好像 goroutine 都執行完了,然而報錯:

fatal error: all goroutines are asleep - deadlock!

為什麼會發生死鎖?goroutine 在退出前調用了 wg.Done() ,程序應該正常退出的。

原因是 goroutine 得到的 "WaitGroup" 變量是 var wg WaitGroup 的一份拷貝值,即 doIt() 傳參只傳值。所以哪怕在每個 goroutine 中都調用了 wg.Done(), 主程序中的 wg 變量並不會受到影響。

1
2
3
4
5func main() {
6    var wg sync.WaitGroup
7    done := make(chan struct{})
8    ch := make(chan interface{})
9
10    workerCount := 2
11    for i := 0; i < workerCount; i++ {
12        wg.Add(1)
13        go doIt(i, ch, done, &wg)    
14    }
15
16    for i := 0; i < workerCount; i++ {  
17        ch <- i
18    }
19
20    close(done)
21    wg.Wait()
22    close(ch)
23    fmt.Println("all done!")
24}
25
26func doIt(workerID int, ch <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
27    fmt.Printf("[%v] is running\n", workerID)
28    defer wg.Done()
29    for {
30        select {
31        case m := <-ch:
32            fmt.Printf("[%v] m => %v\n", workerID, m)
33        case <-done:
34            fmt.Printf("[%v] is done\n", workerID)
35            return
36        }
37    }
38}

運行效果:

32. 向無緩衝的 channel 發送數據,只要 receiver 準備好了就會立刻返回

只有在數據被 receiver 處理時,sender 才會阻塞。因運行環境而異,在 sender 發送完數據後,receiver 的 goroutine 可能沒有足夠的時間處理下一個數據。如:

1func main() {
2    ch := make(chan string)
3
4    go func() {
5        for m := range ch {
6            fmt.Println("Processed:", m)
7            time.Sleep(1 * time.Second) 
8        }
9    }()
10
11    ch <- "cmd.1"
12    ch <- "cmd.2" 
13}

運行效果:

33. 向已關閉的 channel 發送數據會造成 panic

從已關閉的 channel 接收數據是安全的:

接收狀態值 ok 是 false 時表明 channel 中已沒有數據可以接收了。類似的,從有緩衝的 channel 中接收數據,緩存的數據獲取完再沒有數據可取時,狀態值也是 false

向已關閉的 channel 中發送數據會造成 panic:

1func main() {
2    ch := make(chan int)
3    for i := 0; i < 3; i++ {
4        go func(idx int) {
5            ch <- idx
6        }(i)
7    }
8
9    fmt.Println(<-ch)       
10    close(ch)           
11    time.Sleep(2 * time.Second) 
12}

運行結果:

針對上邊有 bug 的這個例子,可使用一個廢棄 channel done 來告訴剩餘的 goroutine 無需再向 ch 發送數據。此時 <- done 的結果是 {}:

1func main() {
2    ch := make(chan int)
3    done := make(chan struct{})
4
5    for i := 0; i < 3; i++ {
6        go func(idx int) {
7            select {
8            case ch <- (idx + 1) * 2:
9                fmt.Println(idx, "Send result")
10            case <-done:
11                fmt.Println(idx, "Exiting")
12            }
13        }(i)
14    }
15
16    fmt.Println("Result: ", <-ch)
17    close(done)
18    time.Sleep(3 * time.Second)
19}

運行效果:

34. 使用了值為 `nil ` 的 channel

在一個值為 nil 的 channel 上發送和接收數據將永久阻塞:

1func main() {
2    var ch chan int 
3    for i := 0; i < 3; i++ {
4        go func(i int) {
5            ch <- i
6        }(i)
7    }
8
9    fmt.Println("Result: ", <-ch)
10    time.Sleep(2 * time.Second)
11}

runtime 死鎖錯誤:

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]

利用這個死鎖的特性,可以用在 select 中動態的打開和關閉 case 語句塊:

1func main() {
2    inCh := make(chan int)
3    outCh := make(chan int)
4
5    go func() {
6        var in <-chan int = inCh
7        var out chan<- int
8        var val int
9
10        for {
11            select {
12            case out <- val:
13                println("---")
14                out = nil
15                in = inCh
16            case val = <-in:
17                println("++++++++++")
18                out = outCh
19                in = nil
20            }
21        }
22    }()
23
24    go func() {
25        for r := range outCh {
26            fmt.Println("Result: ", r)
27        }
28    }()
29
30    time.Sleep(0)
31    inCh <- 1
32    inCh <- 2
33    time.Sleep(3 * time.Second)
34}

運行效果:

35. 若函數 receiver 傳參是傳值方式,則無法修改參數的原有值

方法 receiver 的參數與一般函數的參數類似:如果聲明為值,那方法體得到的是一份參數的值拷貝,此時對參數的任何修改都不會對原有值產生影響。

除非 receiver 參數是 map 或 slice 類型的變量,並且是以指針方式更新 map 中的欄位、slice 中的元素的,才會更新原有值:

1type data struct {
2    num   int
3    key   *string
4    items map[string]bool
5}
6
7func (this *data) pointerFunc() {
8    this.num = 7
9}
10
11func (this data) valueFunc() {
12    this.num = 8
13    *this.key = "valueFunc.key"
14    this.items["valueFunc"] = true
15}
16
17func main() {
18    key := "key1"
19
20    d := data{1, &key, make(map[string]bool)}
21    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
22
23    d.pointerFunc() 
24    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
25
26    d.valueFunc()   
27    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
28}

運行結果:

系列文章

Golang 需要避免踩的 50 個坑

本文轉載自https://github.com/wuYin/blog/blob/master/50-shades-of-golang-traps-gotchas-mistakes.md

相關焦點

  • Golang 需要避免踩的 50 個坑(一)
    最近準備寫一些關於golang的技術博文,本文是之前在GitHub上看到的golang技術譯文,感覺很有幫助,先給各位讀者分享一下。
  • Golang 新手可能會踩的 50 個坑
    前言Go 是一門簡單有趣的程式語言,與其他語言一樣,在使用時不免會遇到很多坑,不過它們大多不是 Go 本身的設計缺陷。如果你剛從其他語言轉到 Go,那這篇文章裡的坑多半會踩到。如果花時間學習官方 doc、wiki、討論郵件、Rob Pike 的大量文章以及 Go 的源碼,會發現這篇文章中的坑是很常見的,新手跳過這些坑,能減少大量調試代碼的時間。初級篇:1-341. 左大括號 { 不能單獨放一行在其他大多數語言中,{ 的位置你自行決定。
  • Golang 新手可能會踩的 50 個坑(初級篇)
    譯文:Golang 新手可能會踩的 50 個坑原文:50 Shades of Go: Traps, Gotchas, and Common Mistakes
  • Golang新手可能會踩的58個坑(上)
    前言Go 是一門簡單有趣的程式語言,與其他語言一樣,在使用時不免會遇到很多坑,不過它們大多不是 Go 本身的設計缺陷。如果你剛從其他語言轉到 Go,那這篇文章裡的坑多半會踩到。如果花時間學習官方 doc、wiki、討論郵件列表、 Rob Pike 的大量文章以及 Go 的源碼,會發現這篇文章中的坑是很常見的,新手跳過這些坑,能減少大量調試代碼的時間。1.1.1.
  • 選品要避免的坑,你踩了幾個?
    因為選品時除了考察商品本身,還要考量跟自己的匹配度,孤立的看商品來選品,是不科學的,要考量匹配的問題,對此,不少經銷商踩過坑,我們來看看有哪些坑?1.圍繞著你公司的主營來選擇商品比較理性,主營業務之外的產品再好,也跟你關係不大,不妨先做個消費者,至於是否要經營它,先冷靜觀察。2. 重疊內耗的坑看到你覺得中意的新品,要先考慮一下與目前已代理品牌的關係,補充還是重複。
  • 「Golang」for range 使用方法及避坑指南
    我個人理解是為了防止切片的元素類型是個指針,如果是個指針的話,直接賦值給v2,隨後如果用戶在循環過程中對其進行修改就會影響到原切片的複製版本的底層數據,這樣是不好的行為(個人猜測,如果有更好的解釋可以留言告知於我,在此表示感謝)。
  • 獲得了「官方自己都會踩的」坑認證:slice 類型內存洩露的邏輯
    101 總結了幾個可能導致內存洩露的場景:https://gfw.go101.org/article/memory-leaking.htmlgoroutine 阻塞在 channel,time.Ticker 不使用但未 stop,以及 for 循環裡用 defer 導致洩露,這三個場景其實已經比較常見了
  • 從油價一夜暴「負」 看衍生品投資怎樣避免踩「坑」
    工程管理碩士項目副主任,浙江大學資本市場研究中心特聘研究員金融風險管理師(FRM)課程預告:主題:從油價一夜暴「負」,看衍生品投資怎樣避免踩「坑」直播大綱●負油價事件回顧● 原油期貨價格為什麼可以是負數?
  • 關乎柴犬一生:怎樣挑選健康的柴犬,姐妹們避免踩坑!
    現在養只柴犬已經成為很多人追隨的潮流,一隻柴犬可以陪伴十幾年,這樣看來,挑選到一隻健康的柴犬是多麼重要的事情,這不僅關乎主人的金錢還關係到柴犬的一生,所以一定要學會挑選柴犬的技巧,避免上當踩坑!二、選擇正規的地方為了避免在買柴犬的路上踩坑,一定要選擇正規的地方。犬舍就是最佳選擇,不管是在柴犬血統純正度以及健康狀況上都比寵物店和狗市好的多,而且犬舍柴犬的價格沒有大家想像中的貴,都是根據品相來定價,絕對不會出現坐地起價現象。
  • 讓你儘可能避免踩坑,陽江海陵島吃住玩樂推薦
    相信很多人在網上都聽說過陽江海陵島很坑,特別是十裡銀灘周邊,村民各自圍地分割海灘收費,吃住價格都是市面上好幾陪,到處都是坑。這次我也體驗了幾天海陵島之旅,是不是與網傳的那麼誇張?說實話小編還沒碰到太嚴重的坑人行為,下面小編就推薦一些廣東陽江海陵島吃住玩樂好地方,儘可能避免踩坑。
  • 高能預警,求職的路上,這3個坑不要去踩,小心吃大虧
    高能預警,求職的路上,這3個坑不要去踩,小心吃大虧現在求職的路上有許許多多的坑在等著別人往下面跳,因為某些原因,總是有人對現在的工作不是很滿意,然後就離職了,踏上了求職之路,但是求職的路上有太多自己不懂的東西,所以難免有時候會踩到一些坑坑窪窪的地方,導致自己摔了一跤,但是人不經過磕磕碰碰怎麼能夠成長起來
  • Golang語言標準庫 sync 包的 Once 怎麼使用?
    o.doSlow(f) }}func (o *Once) doSlow(f func()) {  // 二次檢查時,持有互斥鎖,保證只有一個 goroutine 執行。Done 方法先原子獲取 done 的值,如果 done 的值為 0,則調用 doSlow 方法進行二次檢查,二次檢查時,持有互斥鎖,保證只有一個 goroutine 執行操作,二次檢查結果仍為 0,則認為是第一次執行,程序執行函數類型的參數 f,然後將 done 的值設置為 1。
  • 這5個坑家長不要踩
    02不要給孩子帶熟悉的物品去幼兒園幼兒園剛開學,孩子中午沒有睡午覺的習慣,有的家長就會踩到這個坑,想給孩子帶著他熟悉的物品,比如在家裡用的小枕頭或者是喜歡的玩具,來幫助孩子午睡!其實我自己也差點踩中這個坑!
  • 貓咪做絕育手術需要多少錢?如何避免被坑?
    但許多新手鏟屎官對貓咪絕育手術並不了解,很容易被一些無良的寵物醫院坑錢,因此下面就為大家排一下坑,避免踩坑。雖然簡單,但儘量選擇正規的寵物醫院進行手術,避免術後感染。貓神百科-貓奴必備百科全書二、普通母貓絕育母貓絕育稍微複雜一些,費用也相對較高一些,一線城市費用在800RMB左右
  • 去青島拍攝婚紗照一定要知道的問題,最全婚紗攝影攻略,避免踩坑
    去青島拍攝婚紗照時一件讓人開心、幸福的事情,但是也有一小部分新人難免會遇到一些不良商家,為了讓新人避免踩坑,小編找到了青島婚紗攝影行業口碑一直都名列前茅的青島麗途旅拍攝影工作室的負責人,據了解麗途旅拍通過它高超的攝影技術和細緻的人性化服務,贏得了眾多新人的好評,同時收費透明,定多少錢,拍多少錢,保證絕對沒有隱形消費。
  • 喝茶心得 | 踩坑換來的50條經驗
    以下是我對「茶」的50個理解,或許你也有同樣的體會。 1、沒人能做到對茶100%的了解茶是一個很深奧的門類,想要完全搞懂,沒人能夠做到。6、買茶,越是固執,越容易「踩坑」有些喝了一段時間茶的人,就會非常頑固,不受外界幹擾,堅定的選擇自己認為對的。這樣很容易在一個坑裡踩到很深。因為除非你有專業知識豐富的經驗,否則你對面的茶商一定能找到你的弱點,只要讓你信了,其他就簡單了。這種事,我還真的見過。
  • 繪本的5個坑千萬不要踩!
    所以今天就為寶媽降溫冷靜:繪本的5個大坑千萬不要踩!要是踩了趕緊爬上來!大坑一:買書即是教育我們大人是不是有過這樣的通病,每次信誓旦旦的立好flag,然後費盡心機做出完美計劃後就擱置一旁,仿佛做完計劃事情也就做完了。
  • Golang入門教程——基本操作篇
    在許多語言當中,如果需要返回多個值,往往需要用一個結構體或者是tuple、list等數據結構將它們包裝起來。但是在golang當中支持同時返回多個結果,這將會極大地方便我們的編碼。循環條件分為三個部分,第一個部分是初始化部分,我們對循環體進行初始化,第二個部分是判斷部分,判斷循環結束的終止條件,第三個部分是循環變量的改變部分。
  • 三個股東用技術入股被坑過億,律師教你不踩坑的5招
    但他們的公司章程規定和股東協議不一樣,公司章程寫公司註冊資本是50萬元,雙方都以貨幣出資,小股東認繳出資額22.5萬元,大股東認繳出資額為27.5萬元,而小股東用來繳付出資的22.5萬元是大股東給的。因為公司章程和股東協議的約定不一樣,為後面的紛爭埋下大坑。
  • 驗房注意事項,收房怎麼避免踩坑?請仔細看完
    驗房是很重要的一件事情,因為沒有驗收好,等籤了字在發現問題就很難解決了,肯定避免不了要扯皮。首先要記住的就是一定要看完房子在籤字,有時候你剛剛到交房人員就說:你好,請籤個字然後領取鑰匙驗房。請注意一定要看看籤的到底是什麼,很多時候其實這一步就被套路了,你這時候籤的可能就是驗收合同。