溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

gopl Go程序基礎

發(fā)布時間:2020-07-04 20:52:22 來源:網(wǎng)絡 閱讀:445 作者:騎士救兵 欄目:編程語言

Go 中的名稱

Go 中函數(shù)、變量、常量、類型、語句標簽和包的名稱遵循一個簡單的規(guī)則:名稱的開頭是一個字母(Unicode 中的字符即可)或下劃線,后面可以跟任意數(shù)量的字符、數(shù)字和下劃線,并區(qū)分大小寫。

關(guān)鍵字

共25個關(guān)鍵字,只能用在語法允許的地方,不能作為名稱:

break       //退出循環(huán)
default     //選擇結(jié)構(gòu)默認項(switch、select)
func        //定義函數(shù)
interface   //定義接口
select      //channel
case        //選擇結(jié)構(gòu)標簽
chan        //定義channel
const       //常量
continue    //跳過本次循環(huán)
defer       //延遲執(zhí)行內(nèi)容(收尾工作)
go          //并發(fā)執(zhí)行
map         //map類型
struct      //定義結(jié)構(gòu)體
else        //選擇結(jié)構(gòu)
goto        //跳轉(zhuǎn)語句
package     //包
switch      //選擇結(jié)構(gòu)
fallthrough //switch里繼續(xù)檢查后面的分支
if          //選擇結(jié)構(gòu)
range       //從slice、map等結(jié)構(gòu)中取元素
type        //定義類型
for         //循環(huán)
import      //導入包
return      //返回
var         //定義變量

內(nèi)置預聲明

內(nèi)置的預聲明的常量、類型和函數(shù):

  • 常量
    • true、false
    • iota
    • nil
  • 類型
    • int、int8、int16、int32、int64
    • uint、uint8、uint16、uint32、uint64、uintptr
    • float32、float64、complex128、complex64
    • bool、byte、rune、string、error
  • 函數(shù)
    • make、len、cap、new、append、copy、close、delete
    • complex、real、imag : 復數(shù)相關(guān)
    • panic、recover

這些名稱不是預留的,可以在聲明中使用它們。也可能會看到對其中的名稱進行重聲明,但是要知道這會有沖突的風險。

命名規(guī)則

單詞組合時,使用駝峰式。如果是縮寫,比如:ASCII或HTML,要么全大寫,要么全小寫。比如組合 html 和 escape,可以是下面幾種寫法:

  • htmlEscape
  • HTMLEscape
  • EscapeHTML

但是不推薦這樣的寫法:

  • Escapehtml : 這樣完全區(qū)分不了html是一個詞,所以這樣HTML要全大寫
  • EscapeHtml : 這樣雖然能區(qū)分,但是違反了全大寫或全小寫的建議

基礎數(shù)據(jù)類型

Go的數(shù)據(jù)類型分四大類:

  1. 基礎類型(basic type)
    • 數(shù)字(number)
    • 字符串(string)
    • 布爾型(boolean)
  2. 聚合類型(aggregate type)
    • 數(shù)組(array)
    • 結(jié)構(gòu)體(struct)
  3. 引用類型(reference type)
    • 指針(pointer)
    • 切片(slice)
    • 散列表(map)
    • 函數(shù)(function)
    • 通道(channel)
  4. 接口類型(interface type)

整數(shù)

二元操作符
二元操作符分五大優(yōu)先級,按優(yōu)先級降序排列:

*    /   %   <<  >>  &   &^
+    -   |   ^
==    !=  <   <=  >   >=
&&
||

位運算符
位運算符:

符號 說明 集合
& AND 交集
| OR 并集
^ XOR 對稱差
&^ 位清空(AND NOT) 差集
<< 左移 N/A
>> 右移 N/A

位清空,表達式 z=x&^y ,把y中是1的位在x里對應的那個位,置0。
差集,就是集合x去掉集合y中的元素之后的集合。對稱差則是再加上集合y去掉集合x中的元素的集合,就是前后兩個集合互相求差集,之后再并集。

布爾值

邏輯運算符
邏輯運算符 &&(AND) 以及 ||(OR) 的運算可能引起短路行為:如果運算符左邊的操作數(shù)已經(jīng)能夠直接確定總體結(jié)果,則右邊的操作數(shù)不會做計算。
關(guān)于優(yōu)先級,&& 較 || 優(yōu)先級更高,這里有一個方便記憶的竅門。&& 表示邏輯乘法,|| 表示邏輯加法,這不僅僅指優(yōu)先級,計算結(jié)果也很相似。

布爾轉(zhuǎn)數(shù)值
布爾值無法隱式轉(zhuǎn)換成數(shù)值,反之也不行。如果需要把布爾值轉(zhuǎn)成0或1,需要顯示的使用if:

i := 0
if b {
    i = 1
}

如果轉(zhuǎn)換操作使用頻繁,值得專門寫成一個函數(shù):

func btoi(b bool) int {
    if b {
        return 1
    }
    return 0
}

func itob(i int) bool {
    return i != 0
}

反向轉(zhuǎn)換比較簡單,所以無需專門寫成函數(shù)了。不過為了與btoi對應,上面也寫了一個。

字符串和字節(jié)切片(bytes包)

字節(jié)切片 []byte 類型,其某些屬性和字符串相同。但是由于字符串不可變,因此按增量方式構(gòu)建字符串會導致多次內(nèi)存分配和復制。這種情況下,使用 bytes.Buffer 類型更高效。
bytes 包為高效處理字節(jié)切片提供了 Buffer 類型。Buffer 初始值為空,其大小隨著各種類型數(shù)據(jù)的寫入而增長,如 string、byte 和 []byte。bytes.Buffer 變量無須初始化,其零值有意義:

package main

import (
    "bytes"
    "fmt"
)

// 函數(shù) intsToString 與 fmt.Sprintf(values) 類似,但插入了逗號
func intsToString(values []int) string {
    var buf bytes.Buffer
    buf.WriteByte('[')
    for i, v := range values {
        if i > 0 {
            buf.WriteString(", ")
        }
        fmt.Fprintf(&buf, "%d", v)
    }
    buf.WriteByte(']')
    return buf.String()
}

func main() {
    fmt.Println(intsToString([]int{1, 2, 3}))  // "[1, 2, 3]"
    fmt.Println([]int{1, 2, 3})  // "[1 2 3]"
}

復合數(shù)據(jù)類型

有四種復合數(shù)據(jù)類型:

  • 數(shù)組
  • 切片(slice)
  • map
  • 結(jié)構(gòu)體

切片(slice)

反轉(zhuǎn)和平移
就地反轉(zhuǎn)slice中的元素:

package main

import "fmt"

func reverse(s []int) {
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}

func main() {
    l := [...]int{1, 2, 3, 4, 5} // 這個是數(shù)組
    fmt.Println(l)
    reverse(l[:]) // 傳入切片
    fmt.Println(l)
}

將一個切片向左平移n個元素的簡單方法是連續(xù)調(diào)用三次反轉(zhuǎn)函數(shù)。第一次反轉(zhuǎn)前n個元素,第二次返回剩下的元素,最后整體做一次反轉(zhuǎn):

func moveLeft(n int, s []int) {
    reverse(s[:n])
    reverse(s[n:])
    reverse(s)
}

func moveRight(n int, s []int) {
    reverse(s[n:])
    reverse(s[:n])
    reverse(s)
}

切片的比較
與數(shù)組不同,切片無法做比較。標準庫中提供了高度優(yōu)化的函數(shù) bytes.Equal 來比較兩個字節(jié)切片([]byte)。但是對其他類型的切片,Go不支持比較。當然自己寫一個比較的函數(shù)也不難:

func equal(x, y []string) bool {
    if len(x) != len(y) {
        return false
    }
    for i := range x {
        if x[i] != y[i] {
            return false
        }
    }
    return true
}

上面的方法也只是返回執(zhí)行函數(shù)當時的結(jié)果,但是切片的底層數(shù)組可以能發(fā)生改變,在不同的時間切片所擁有的元素可能不同,不能保證整個生命周期都保持不變??傊珿o不允許直接比較切片。

初始化
像切片和map這類引用類型,使用前是需要初始化的。僅僅進行聲明,是不分配內(nèi)存的,此時值為nil。
完成初始化后(大括號或者make函數(shù)),此時就是已經(jīng)完成了初始化,分配內(nèi)存空間,值不為nil。

和nil比較
切片唯一允許的比較操作是和nil做比較。值為nil的切片長度和容量都是零,但是也有非nil的切片長度和容量也都是零的:

func main() {
    var s []int
    fmt.Println(s == nil)  // true
    s = nil
    fmt.Println(s == nil)  // true
    s = []int(nil)
    fmt.Println(s == nil)  // true
    s = []int{}
    fmt.Println(s == nil)  // flase
}

所以要檢查一個切片是否為空,應該使用 len(s) == 0,而不是和nil做比較。
另外,值為nil的切片其表現(xiàn)和其它長度為零的切片是一樣的。無論值是否為nil,GO的函數(shù)都應該以相同的方式對待所有長度為零的切片。

map

引用類型
因為map類型是間接的指向它的 key/value 對,所以函數(shù)或方法對引用本身做的任何改變,比如設置值為 nil 或者使它指向一個不同的 map,都不會在調(diào)用者身上產(chǎn)生作用:

package main

import "fmt"

type map1 map[string]string

func change(m map1) {
    fmt.Println("change:", m) // change: map[k1:v1]
    m = map1{"k1": "v2"} // 將m指向一個新的map,但是并不會改變main中m1的值
    fmt.Println("change:", m) // change: map[k1:v2]
}

func main() {
    m1 := map1{"k1": "v1"}
    fmt.Println("main:", m1) // main: map[k1:v1]
    change(m1) // m1 的值不會改變
    fmt.Println("main", m1) // main map[k1:v1]
}

main函數(shù)中創(chuàng)建了m1,然后把m1傳遞給change函數(shù),引用類型傳的是存儲了m1的內(nèi)存地址的副本。在change中修改m的值,指向了一個新創(chuàng)建的map,此時m就指向了新創(chuàng)建的map的內(nèi)存地址?;氐絤ain函數(shù)中m1指向的內(nèi)存地址并沒有改變,而該地址對應的map的內(nèi)容也沒有改變。
下面這個函數(shù),main函數(shù)中原來的map是會改變的。main函數(shù)中map的指向的地址沒有變,但是地址對應的數(shù)據(jù)發(fā)生了變化:

func changeKeyValue(m map1, k, v string) {
    fmt.Println("change:", m)
    m[k] = v
    fmt.Println("change:", m)
}

使用切片做key
切片是不能作為key的,并且切片是不可比較的,不過可以有一個間接的方法來實現(xiàn)切片作key。定義一個幫助函數(shù)k,將每一個key都映射到字符串:

var m = make(map[string]int)

func k(list []string) string { fmt.Sprint("%q", list) }

func Add(list []string) { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }

這里使用%q來格式化切片,就是包含雙引號的字符串,所以(["ab", "cd"] 和 ["abcd"])是不一樣的。就是,當且僅當 x 和 y 相等的時候,才認為 k(x)==k(y)。
同樣的方法適用于任何不可直接比較的key類型,不僅僅局限于切片。同樣,k(x) 的類型不一定是字符串類型,任何能夠得到想要的比較結(jié)果的可比較類型都可以。

集合
Go 沒有提供集合類型,但是利用key唯一的特點,可以用map來實現(xiàn)這個功能。比如說字符串的集合:

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    seen := make(map[string]bool) // 字符串集合
    input := bufio.NewScanner(os.Stdin)
    for input.Scan() {
        line := input.Text()
        if !seen[line] {
            seen[line] = true
            fmt.Println("Set:", line)
        }
    }
    if err := input.Err(); err != nil {
        fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
        os.Exit(1)
    }
}

從標準輸出獲取字符串,用map來存儲已經(jīng)出現(xiàn)過的行,只有首次出現(xiàn)的字符串才會打印出來。

使用空結(jié)構(gòu)體作value
上面的集合中使用bool來作為map的value,而bool也有true和false兩種值,而實際只使用了1種值。
這里還可以使用空結(jié)構(gòu)體(類型:struct{}、值:struct{}{})??战Y(jié)構(gòu)體,沒有長度,也不攜帶任何信息,用它可能是最合適的。但由于這種方式節(jié)約的內(nèi)存很少并且語法復雜,所以一般盡量避免這樣使用。

位向量集合

Go 語言的集合通常使用 map[T]bool 來實現(xiàn),其中T是元素類型。使用 map 的集合擴展性良好,但是對于一些特定的問題,一個專門設計過的集合性能會更優(yōu)。比如,在數(shù)據(jù)流分析領(lǐng)域,集合元素都是小的非負整型,集合擁有許多元素,而且集合的操作多數(shù)是求并集和交集,位向量是個理想的數(shù)據(jù)結(jié)構(gòu)。

基礎類型和方法

位向量使用一個無符號整型值的切片,每一位代表集合中的一個元素。如果設置第 i 位的元素,則表示集合包含 i。下面是一個包含了三個方法的簡單位向量類型:

package intset

import (
    "bytes"
    "fmt"
)

// 這是一個包含非負整數(shù)的集合
// 零值代表空的集合
type IntSet struct {
    words []uint64
}

// 集合中是否存在非負整數(shù)x
func (s *IntSet) Has(x int) bool {
    word, bit := x/64, uint(x%64)
    return word < len(s.words) && s.words[word]&(1<<bit) != 0
}

// 添加一個數(shù)x到集合中
func (s *IntSet) Add(x int) {
    word, bit := x/64, uint(x%64)
    for word >= len(s.words) {
        s.words = append(s.words, 0)
    }
    s.words[word] |= 1 << bit
}

// 求并集,并保存到s中
func (s *IntSet) UnionWith(t *IntSet) {
    for i, tword := range t.words {
        if i < len(s.words) {
            s.words[i] |= tword
        } else {
            s.words = append(s.words, tword)
        }
    }
}

// 以字符串"{1 2 3}"的形式返回集合
func (s *IntSet) String() string {
    var buf bytes.Buffer
    buf.WriteByte('{')
    for i, word := range s.words {
        if word == 0 {
            continue
        }
        for j := 0; j < 64; j++ {
            if word&(1<<uint(j)) != 0 {
                if buf.Len() > len("{") {
                    buf.WriteByte(' ')
                }
                fmt.Fprintf(&buf, "%d", 64*i+j)
            }
        }
    }
    buf.WriteByte('}')
    return buf.String()
}

每一個 word 有64位,為了定位第 x 位的位置,通過 x/64 結(jié)果取整,就是 word 的索引,而 x%64 取模運算是 word 內(nèi)位的索引。
這里還自定義了以字符串輸出 IntSet 的方法,就是一個 String 方法。在 String 方法中 bytes.Buffer 經(jīng)常以這樣的方式用到。
因為 Add 方法和 UnionWith 方法需要對 s.word 進行賦值,所以需要用到指針。所以該類型的其他方法也都使用了指針,就是 Has 方法和 String 方法是不需要使用指針的,但是為了保持一致,就都使用指針作為方法的接收者。

并集、交集、差集、對稱差

上面只給了并集的示例,這里提到的4種集合的計算,簡單參考一下前面的“位運算符”的介紹,很簡單的通過修改一下位運算的符號就能實現(xiàn)了。
并集和對稱差,需要把s.words中沒有的而t.words中多的那些元素全部加進來。而交集和差集,直接無視這部分元素就好了:

// 并集 Union,上面的示例中已經(jīng)有了
func (s *IntSet) UnionWith(t *IntSet) {
    for i, tword := range t.words {
        if i < len(s.words) {
            s.words[i] |= tword
        } else {
            s.words = append(s.words, tword)
        }
    }
}

// 交集 Intersection
func (s *IntSet) IntersectionWith(t *IntSet) {
    for i, tword := range t.words {
        if i < len(s.words) {
            s.words[i] &= tword
        }
    }
}

// 差集 Difference
func (s *IntSet) DifferenceWith(t *IntSet) {
    for i, tword := range t.words {
        if i < len(s.words) {
            s.words[i] &^= tword
        }
    }
}

// 對稱差 SymmetricDifference
func (s *IntSet) SymmetricDifferenceWith(t *IntSet) {
    for i, tword := range t.words {
        if i < len(s.words) {
            s.words[i] ^= tword
        } else {
            s.words = append(s.words, tword)
        }
    }
}

把這里的三個新的方法添加到最初定義的包中就可以使用。

計算置位個數(shù)

就是統(tǒng)計集合中元素的總數(shù),下面分別講3種實現(xiàn)的算法:

  1. 查表法:空間換時間。
  2. 右移循環(huán)算法:最簡單,最容易想到。
  3. 快速法:如果輸入整數(shù)中“1”遠小于“0”(稀疏),可以通過一些針對性算法來提高效率。

查表法
先使用 init 函數(shù)來針對每一個可能的8位值預計算一個結(jié)果表 pc,這樣之后只需要將每次快查表的結(jié)果相加而不用進行一步步的計算:

// pc[i] 是 i 的 population count
var pc [256]byte

func init() {
    for i := range pc {
        pc[i] = pc[i/2] + byte(i&1)
    }
}

// 返回元素個數(shù),查表法
func (s *IntSet) Len() int {
    var counts int
    for _, word := range s.words {
        counts += int(pc[byte(word>>(0*8))])
        counts += int(pc[byte(word>>(1*8))])
        counts += int(pc[byte(word>>(2*8))])
        counts += int(pc[byte(word>>(3*8))])
        counts += int(pc[byte(word>>(4*8))])
        counts += int(pc[byte(word>>(5*8))])
        counts += int(pc[byte(word>>(6*8))])
        counts += int(pc[byte(word>>(7*8))])
    }
    return counts
}

右移循環(huán)算法
在其實際參數(shù)的位上執(zhí)行移位操作,每次判斷最右邊的位,進而實現(xiàn)統(tǒng)計功能:

// 返回元素個數(shù),右移循環(huán)算法
func (s *IntSet) Len2() int {
    var count int
    for _, x := range s.words {
        for x != 0 {
            if x & 1 == 1 {
                count++
            }
            x >>= 1
        }
    }
    return count
}

快速法:
使用 x&(x-1) 可以清除x最右邊的非零位,不停地進行這個運算直到數(shù)值變成0。其中進行了幾次運行就表示有幾個1了:

// 返回元素個數(shù),快速法
func (s *IntSet) Len3() int {
    var count int
    for _, x := range s.words {
        for x != 0 {
            x = x & (x - 1)
            count++
        }
    }
    return count
}

添加其他方法

繼續(xù)為我們的位向量類型添加其他方法:

// 一次添加多個元素
func (s *IntSet) AddAll(nums ...int) {
    for _, x := range nums {
        s.Add(x)
    }
}

// 移除元素,無論是否在集合中,都把該位置置0
func (s *IntSet) Remove(x int) {
    word, bit := x/bitCounts, uint(x%bitCounts)
    if word < len(s.words) {
        s.words[word] &^= 1 << bit
    }
    // 移除高位全零的元素
    for i := len(s.words)-1; i >=0; i-- {
        if s.words[i] == 0 {
            s.words = s.words[:i]
        } else {
            break
        }
    }
}

// 刪除所有元素
func (s *IntSet) Clear() {
    *s = IntSet{}
}

// 返回集合的副本
func (s *IntSet) Copy() *IntSet {
    x := IntSet{words: make([]uint, len(s.words))}
    copy(x.words, s.words)
    return &x
}

// 返回包含集合元素的 slice,這適合在 range 循環(huán)中使用
func (s *IntSet) Elems() []int {
    var ret []int
    for i, word := range s.words {
        if word == 0 {
            continue
        }
        for j := 0; j < bitCounts; j++ {
            if word&(1<<uint(j)) != 0 {
                ret = append(ret, bitCounts*i+j)
            }
        }
    }
    return ret
}

自適應32或64位平臺

這里每個字的類型都是 uint64,但是64位的計算在32位的平臺上的效率不高。使用 uint 類型,這是適合平臺的無符號整型。除以64的操作可以使用一個常量來代表32位或64位。
這里有一個討巧的表達式: 32<<(^uint(0)>>63) 。在不同的平臺上計算的結(jié)果就是32或64。

const bitCounts = 32 << (^uint(0) >> 63) // 使用這個常量去做取模和取余的計算

對應的要把代碼中原本直接使用數(shù)字常量64的地方替換成這個常量,比如 Has 方法:

const bitCounts = 32 << (^uint(0) >> 63) // 32位平臺這個值就是32,64位平臺這個值就是64

// 集合中是否存在非負整數(shù)x
func (s *IntSet) Has(x int) bool {
    word, bit := x/bitCounts, uint(x%bitCounts)
    return word < len(s.words) && s.words[word]&(1<<bit) != 0
}
向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI