您好,登錄后才能下訂單哦!
Go 中函數(shù)、變量、常量、類型、語句標簽和包的名稱遵循一個簡單的規(guī)則:名稱的開頭是一個字母(Unicode 中的字符即可)或下劃線,后面可以跟任意數(shù)量的字符、數(shù)字和下劃線,并區(qū)分大小寫。
共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)置的預聲明的常量、類型和函數(shù):
這些名稱不是預留的,可以在聲明中使用它們。也可能會看到對其中的名稱進行重聲明,但是要知道這會有沖突的風險。
單詞組合時,使用駝峰式。如果是縮寫,比如:ASCII或HTML,要么全大寫,要么全小寫。比如組合 html 和 escape,可以是下面幾種寫法:
但是不推薦這樣的寫法:
Go的數(shù)據(jù)類型分四大類:
二元操作符
二元操作符分五大優(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é)切片 []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ù)類型:
反轉(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類型是間接的指向它的 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)
}
}
}
把這里的三個新的方法添加到最初定義的包中就可以使用。
就是統(tǒng)計集合中元素的總數(shù),下面分別講3種實現(xiàn)的算法:
查表法
先使用 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
}
這里每個字的類型都是 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
}
免責聲明:本站發(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)容。