溫馨提示×

溫馨提示×

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

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

gopl Go程序基礎

發布時間:2020-07-04 20:52:22 來源:網絡 閱讀:462 作者:騎士救兵 欄目:編程語言

Go 中的名稱

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

關鍵字

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

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

內置預聲明

內置的預聲明的常量、類型和函數:

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

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

命名規則

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

  • htmlEscape
  • HTMLEscape
  • EscapeHTML

但是不推薦這樣的寫法:

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

基礎數據類型

Go的數據類型分四大類:

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

整數

二元操作符
二元操作符分五大優先級,按優先級降序排列:

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

位運算符
位運算符:

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

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

布爾值

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

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

i := 0
if b {
    i = 1
}

如果轉換操作使用頻繁,值得專門寫成一個函數:

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

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

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

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

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

package main

import (
    "bytes"
    "fmt"
)

// 函數 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]"
}

復合數據類型

有四種復合數據類型:

  • 數組
  • 切片(slice)
  • map
  • 結構體

切片(slice)

反轉和平移
就地反轉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} // 這個是數組
    fmt.Println(l)
    reverse(l[:]) // 傳入切片
    fmt.Println(l)
}

將一個切片向左平移n個元素的簡單方法是連續調用三次反轉函數。第一次反轉前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)
}

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

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
}

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

初始化
像切片和map這類引用類型,使用前是需要初始化的。僅僅進行聲明,是不分配內存的,此時值為nil。
完成初始化后(大括號或者make函數),此時就是已經完成了初始化,分配內存空間,值不為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的切片其表現和其它長度為零的切片是一樣的。無論值是否為nil,GO的函數都應該以相同的方式對待所有長度為零的切片。

map

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

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函數中創建了m1,然后把m1傳遞給change函數,引用類型傳的是存儲了m1的內存地址的副本。在change中修改m的值,指向了一個新創建的map,此時m就指向了新創建的map的內存地址?;氐絤ain函數中m1指向的內存地址并沒有改變,而該地址對應的map的內容也沒有改變。
下面這個函數,main函數中原來的map是會改變的。main函數中map的指向的地址沒有變,但是地址對應的數據發生了變化:

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

使用切片做key
切片是不能作為key的,并且切片是不可比較的,不過可以有一個間接的方法來實現切片作key。定義一個幫助函數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) 的類型不一定是字符串類型,任何能夠得到想要的比較結果的可比較類型都可以。

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

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來存儲已經出現過的行,只有首次出現的字符串才會打印出來。

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

位向量集合

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

基礎類型和方法

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

package intset

import (
    "bytes"
    "fmt"
)

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

// 集合中是否存在非負整數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
}

// 添加一個數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 結果取整,就是 word 的索引,而 x%64 取模運算是 word 內位的索引。
這里還自定義了以字符串輸出 IntSet 的方法,就是一個 String 方法。在 String 方法中 bytes.Buffer 經常以這樣的方式用到。
因為 Add 方法和 UnionWith 方法需要對 s.word 進行賦值,所以需要用到指針。所以該類型的其他方法也都使用了指針,就是 Has 方法和 String 方法是不需要使用指針的,但是為了保持一致,就都使用指針作為方法的接收者。

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

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

// 并集 Union,上面的示例中已經有了
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)
        }
    }
}

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

計算置位個數

就是統計集合中元素的總數,下面分別講3種實現的算法:

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

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

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

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

// 返回元素個數,查表法
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
}

右移循環算法
在其實際參數的位上執行移位操作,每次判斷最右邊的位,進而實現統計功能:

// 返回元素個數,右移循環算法
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最右邊的非零位,不停地進行這個運算直到數值變成0。其中進行了幾次運行就表示有幾個1了:

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

添加其他方法

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

// 一次添加多個元素
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 循環中使用
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) 。在不同的平臺上計算的結果就是32或64。

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

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

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

// 集合中是否存在非負整數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問一下細節

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

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女