内容简介:Go 中函数、变量、常量、类型、语句标签和包的名称遵循一个简单的规则:名称的开头是一个字母(Unicode 中的字符即可)或下划线,后面可以跟任意数量的字符、数字和下划线,并区分大小写。共25个关键字,只能用在语法允许的地方,不能作为名称:内置的预声明的常量、类型和函数:
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的数据类型分四大类:
-
基础类型(basic type)
- 数字(number)
- 字符串(string)
- 布尔型(boolean)
-
聚合类型(aggregate type)
- 数组(array)
- 结构体(struct)
-
引用类型(reference type)
- 指针(pointer)
- 切片(slice)
- 散列表(map)
- 函数(function)
- 通道(channel)
- 接口类型(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的内存地址。回到main函数中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来存储已经出现过的行,只有首次出现的字符串才会打印出来。
上面的集合中使用bool来作为map的value,而bool也有true和false两种值,而实际只使用了1种值。
这里还可以使用空结构体(类型:struct{}、值:struct{}{})。空结构体,没有长度,也不携带任何信息,用它可能是最合适的。但由于这种方式节约的内存很少并且语法复杂,所以一般尽量避免这样使用。
位向量集合
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”远小于“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 }
以上所述就是小编给大家介绍的《Go程序基础》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【小程序】---- 基础知识汇总
- 编写高质量Python程序(三)基础语法
- TCP/IP基础之应用程序接口
- Go36-48,49-程序性能分析基础
- Java 程序员必读核心书单(基础版)
- Go基础学习记录 - 编写Web应用程序 - 路由和程序启动的一些思考
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript入门经典
Paul Wilton、Jeremy McPeak / 施宏斌 / 清华大学出版社 / 2009-2 / 98.00元
《Java Script入门经典(第3版)》首先介绍了J avaScript的基本语法,并介绍了如何发挥JavaScript中对象的威力。《Java Script入门经典(第3版)》还介绍了如何操纵最新版本浏览器所提供的BOM对象。在《Java Script入门经典(第3版)》的高级主题中,将介绍如何使用cookie,以及如何应用DHTML技术使Web页面焕发动感和活力。另外,《Java Scri......一起来看看 《JavaScript入门经典》 这本书的介绍吧!