引言 最近迷上了Go语言, 看完《The Go Programming Language》后收益颇丰。作为一个极其懒惰 的程序猿, 我比较认同Golang提倡的”Less is more”的哲学, 而且Golang确实是一门非常有意思的语言, 集众家之长而又标新立异. 本系列记录一下在学习实践Go语言过程中使用到的一些代码片段,边学边记, 不仅限于标准库, 还包括真正开发过程中常用的各类组件的用法 。
Go 语言特性 作为一个更熟悉其他编程语言的猿, 初入Golang发现有很多跟我之前用的一些主流编程语言差异很大的特性, 在开始编写Golang程序之前, 首先需要铭记 这些特性:
没有继承
没有重载
没有泛型
没有枚举
没有class / private / protected / public / static
没有getter / setter
没有函数参数默认值
没有三元判断操作符
没有lambda表达式
没有try / catch / finally
没有线程
没有while循环
不存在引用传递, 传指针也是复制指针值
switch / case 不需要写break
if / for 不需要加括号
另外, 牢记 这些Go中常用关键字, 内置函数, 类型等
make() / len() / cap() / append() / copy() delete() / close()
const / iota / rune / interface / struct / map
range / select / chan / close / go
defer / panic
注意:关于值传递和引用传递,有一个非常简单的解释:能否有两个变量指向同一个内存地址 ,比如在C++是存在引用传递的,而Java是不存在引用传递的的。
Golang同样不存在引用传递 。比如下面两种形式的值传递,第二个func传递的值是对象的地址,两种方式都可以用点号访问,地址传过去也不需要C++的 “->” 符号访问成员变量和方法。星号(指针)和取址操作符(&)与C/C++类似。
1 2 3 4 5 6 7 8 9 10 11 12 type Example struct { property string } type Param struct {}func (example Example) passCopyOfCaller (passCopyOfParam Param) { } func (example *Example) passAddressCaller (passAddressOfParam *Param) { }
索引 由于代码比较多, 这个系列将拆分为多篇连载, 以下是各篇索引链接:
除了这些代码片段, 有些Golang的重要内容不仅限于代码. 比如测试, 调试调优, 并发编程等等, 单独写了一些文章, 链接如下:
参考链接 此处顺便记录一下学习Go语言过程中比较有用的一些资源链接, 包括部分代码片段的参考链接
概述 本篇要记录一些入门级的基本操作和Golang中常用代码逻辑写法, 如数据结构定义, 对象操作, 日期时间, 枚举, 异常处理, 类型转换等等. 以及一些未归类的杂项, 比如Slice的用法, Unsafe指针操作, 反射等等。
日常编程实用片段 错误处理 先从Golang代码中出现频率最高的语句开始 :)
枚举定义 Golang没有枚举,一般使用常量定义”枚举”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" ) type StateEnum uint const ( A StateEnum = 1 << iota B C ) const ( X, Y = iota , iota + 1 J, K M, N = 10 , 20 R = iota << 4 S P ) func main () { state := StateEnum(1 ) fmt.Println(state == A) fmt.Printf("%b %b %b \n" , A, B, C) fmt.Printf("%03b %b %b \n" , A|B, B|C, A|B|C) fmt.Println(X, Y, M, N, R, S, P) }
日期时间 标准库time包的使用,包括定时器,日期时间以及时区的转换处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package mainimport ( "fmt" "time" ) func main () { t := time.Now() fmt.Printf("current time: %v \n" , t) tick := time.Tick(1 * time.Second) time.Sleep(1 * time.Second) go func () { for range tick { fmt.Println("tick" ) } }() time.Sleep(1000 * time.Millisecond) layout := "2006-01-02 15:04:05" fmt.Printf("%s \n" , t.Format("2006-01-02 15:04:05PM MST" )) t2, _ := time.Parse(layout, "2018-01-01 20:05:30" ) t3, _ := time.ParseInLocation(layout, "2018-01-01 20:05:30" , time.Local) pacific, _ := time.LoadLocation("America/Los_Angeles" ) t4, _ := time.ParseInLocation(layout, "2018-01-01 20:05:30" , pacific) fmt.Printf("%s %s %s \n" , t2.Format(time.RFC3339), t3.Format(time.RFC3339), t4.Format(time.RFC3339)) var span time.Duration span, _ = time.ParseDuration("+10ms" ) tn := t2.Add(span) span2 := tn.Sub(t2) span2 += 10 * time.Millisecond fmt.Printf("%v, %v \n" , span, span2) time.Since(t2) <-time.NewTimer(10 * time.Second).C }
常用的开源库之一,定时任务Cron Job
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package mainimport ( "fmt" "time" "github.com/robfig/cron" ) type MyScheduleJob struct {}func (MyScheduleJob) Next (time.Time) time .Time { fmt.Println("calc next" ) return time.Now().Add(10 * time.Second) } func (MyScheduleJob) Run () { fmt.Println("run job" ) } func main () { tz, _ := time.LoadLocation("Asia/Chongqing" ) c := cron.NewWithLocation(tz) c.AddFunc("0 30 * * * *" , func () { fmt.Println("Every hour on the half hour" ) }) c.AddFunc("@hourly" , func () { fmt.Println("Every hour" ) }) c.AddFunc("@every 1h30m" , func () { fmt.Println("Every hour thirty" ) }) c.Start() c.AddFunc("@daily" , func () { fmt.Println("Every day" ) }) entries := c.Entries() fmt.Printf("%v \n" , entries[0 ].Next) fmt.Printf("%v \n" , c.Location()) j := MyScheduleJob{} c.Schedule(j, j) fmt.Scanln() }
集合排序 标准库sort包排序以及自定义排序规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 package mainimport ( "bytes" "fmt" "io/ioutil" "sort" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/transform" ) type ByPinyin []string func (s ByPinyin) Len () int { return len (s) }func (s ByPinyin) Swap (i, j int ) { s[i], s[j] = s[j], s[i] }func (s ByPinyin) Less (i, j int ) bool { a, _ := UTF82GBK(s[i]) b, _ := UTF82GBK(s[j]) bLen := len (b) for idx, chr := range a { if idx > bLen-1 { return false } if chr != b[idx] { return chr < b[idx] } } return true } func UTF82GBK (src string ) ([]byte , error) { GB18030 := simplifiedchinese.All[0 ] return ioutil.ReadAll(transform.NewReader(bytes.NewReader([]byte (src)), GB18030.NewEncoder())) } func GBK2UTF8 (src []byte ) (string , error) { GB18030 := simplifiedchinese.All[0 ] bytes, err := ioutil.ReadAll(transform.NewReader(bytes.NewReader(src), GB18030.NewDecoder())) return string (bytes), err } func main () { a := []int {4 , 3 , 2 , 1 , 5 , 9 , 8 , 7 , 6 } b := []string {"哈" , "呼" , "嚯" , "ha" , "," } sort.Ints(a) fmt.Println("After sorted: " , a) sort.Slice(a, func (i, j int ) bool { return a[i] > a[j] }) fmt.Println("DESC: " , a) sort.Strings(b) fmt.Println("Default: " , b) sort.Sort(ByPinyin(b)) fmt.Println("By PinYin: " , b) sort.Sort(sort.Reverse(ByPinyin(b))) fmt.Println("Reverse: " , b) idx := sort.Search(len (b), func (i int ) bool { return b[i] == "哈" }) fmt.Println(idx) }
集合的创建和修改 Golang中大部分场景使用切片数据类型(Slice) 进行集合操作, 只有初始化指定了长度的数组才是数组类型 数组或切片都不能越界访问, 否在会报错: index out of range / slice bounds out of range内置函数或切片语法(make cap len copy append delete s[m:n] … ) 能够支持简单的集合操作, 但复杂的操作一般自己封装遍历方法,或借助第三方库实现,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package mainimport ( "fmt" ) func main () { var arr []string fmt.Printf("%p length: %d capacity: %d \n" , arr, len (arr), cap (arr)) arr = make ([]string , 100 , 200 ) fmt.Printf("%p length: %d capacity: %d \n" , arr, len (arr), cap (arr)) arr = append (arr, fmt.Sprintf("element-1" )) index := 50 arr = append (arr[:index], arr[index+1 :]...) rear := append ([]string {}, arr[index+1 :]...) arr = append (arr[:index], "test" ) arr = append (arr[:index], rear...) str := []byte ("hello world" ) copy (str, "haha " ) fmt.Println(string (str)) }
结构体/对象的复制 对象的浅拷贝(Shadow Copy)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type PlainObj struct { str string sub SubObj } type SubObj struct { substr string } func main () { src := &PlainObj{"str" , SubObj{"substr" }} target := &PlainObj{} *target = *src target.sub.substr = "newstr" fmt.Println(src) fmt.Println(target) }
对象的深拷贝
1 2 3 4 5 6 7 8 func deepCopy (dst, src interface {}) error { var buf bytes.Buffer if err := gob.NewEncoder(&buf).Encode(src); err != nil { return err } return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst) }
类型转换及类型断言 在字符串的转换中,rune类型是Golang比较有特色的地方,字符串强转成[]byte就是字节 数组,UTF-8中文字符大部分会变成3个Byte;而强转成[]rune类型则是字符 数组,每个值表示一个Unicode,而UTF-8是最常见的实现方式,因此rune数组输出出来等于Unicode码点 ,而真实存储的是变长的UTF8表示的byte数组,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 str := "字符串 string" sliceByte := []byte (str) sliceRune := []rune (str) fmt.Println(sliceByte) fmt.Println(sliceRune[:1 ]) fmt.Println(string (sliceRune[2 :10 ])) var num1 int32 = 1 var _ int64 = int64 (num1)func (arg interface {}) { switch arg.(type ) { case string : fmt.Printf("ref str: %v %T \r\n" , arg, arg) case int : fmt.Printf("ref int: %v %T \r\n" , arg, arg) default : fmt.Printf("unknown type: %T" , arg) } if str, ok := arg.(string ); ok { fmt.Printf("%v " , str) } else if num, ok := arg.(int ); ok { fmt.Printf("%v " , num) } }("any value" )
panic的处理和恢复 一般的异常可以返回值判断error,但fatal error,一般用panic + recover的方式处理,defer来确保所有退出条件都会执行,defer其实是在return之后执行的,比较违反常识,细节参考: https://blog.csdn.net/qq_22063697/article/details/74892728
注:因为Runtime遇到defer的函数是压栈的,所以return之后当前栈弹出,再依次后进先出执行defer的函数列表,这就解释了为什么是在return之后执行,与很多语言的try-catch-finally在同一个调用栈是不一样的
Golang错误和异常处理的规范和原则: https://www.jianshu.com/p/f30da01eea97
1 2 3 4 5 6 7 8 defer func () { if err := recover (); err != nil { fmt.Printf("\r\nPanic recover! p: %v %s \r\n" , err, debug.Stack()) } }()
杂项 指针操作 不安全的指针操作与直接运算 (非常危险, 尽量不要用)
1 2 3 4 5 6 7 8 obj1 := &PlainObj{str: "2" } obj2 := (*PlainObj2)(unsafe.Pointer(obj1)) ptr := uintptr (unsafe.Pointer(obj1)) ptr += unsafe.Sizeof(int32 (0 ))
interface与反射操作 因为Golang是Duck Type多态,比Java/C#这种通过层层接口继承的方式要灵活很多。多态是OOP中常用的概念,即同一个函数/方法,在不同对象时表现出来不同的行为 ,Golang有两种方式实现:
第一种方式:定义一个interface,不同的对象只要有这个interface定义的方法就可以调用,这种多态与Java/C#这种传统OOP语言思路完全不一样
第二种方式:直接传递interface{}参数(类似C#的dynamic类型),通过类型断言,反射来处理。这可能带来interface{} 满天飞,到处是类型转换和类型断言的问题,所以能用第一种方式做就用第一种方式,不要滥用interface{}
第一种方式的例子,来自 https://gobyexample.com/interfaces
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package mainimport "fmt" import "math" type geometry interface { area() float64 } type rect struct { width, height float64 } type circle struct { radius float64 } func (r rect) area () float64 { return r.width * r.height } func (c circle) area () float64 { return math.Pi * c.radius * c.radius } func measure (g geometry) { fmt.Println(g.area()) } func main () { r := rect{width: 3 , height: 4 } c := circle{radius: 5 } measure(r) measure(c) }
第二种方式,func measure(g interface{}) interface{}的类型断言[ x.(Type) ]在前几节有示例了, 这里再尝试一下标准库reflect包的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 package mainimport ( "fmt" "reflect" ) type Ref struct { Expose int } func (*Ref) DoSth () { fmt.Println("reflect !" ) } func main () { refMap := make (map [string ]interface {}) refMap["a" ] = Ref{1 } refMap["b" ] = &Ref{2 } fmt.Printf("1. Finish init map %d %v\r\n" , len (refMap), refMap) typeOfA := reflect.TypeOf(refMap["a" ]) valueOfB := reflect.ValueOf(refMap["b" ]) fmt.Println("\n2. Print Name() Kine() String()" ) fmt.Println("refMap['a'] type is: " , typeOfA, typeOfA.Name()) fmt.Println("refMap['b'] kind is: " , reflect.TypeOf(refMap["b" ]).Kind()) fmt.Println("refMap['b'] value: " , valueOfB) fmt.Println("\n3. MethodByName() and Call()" ) method := valueOfB.MethodByName("DoSth" ) method.Call([]reflect.Value{}) fmt.Println("\n4. NumField() and Field() / FieldByName()" ) for i := 0 ; i < typeOfA.NumField(); i++ { field := typeOfA.Field(i) fmt.Printf("field '%s' type is :%s\n" , field.Name, field.Type) } var _ = valueOfB.Interface() fmt.Println("\n5. NumField() and Field() for struct pointer" ) for i := 0 ; i < valueOfB.Elem().NumField(); i++ { value := valueOfB.Elem().Field(i) value.SetInt(33 ) fmt.Printf("canset: %t value: %s \n" , value.CanSet(), value.String()) } fmt.Println("\n6. Invoke Function by Call()" ) for i := 0 ; i < valueOfB.NumMethod(); i++ { method := valueOfB.Method(i) method.Call([]reflect.Value{}) } }
标准库container包的数据结构 标准库container包的有一些常用的数据结构的实现比如链表, 环形队列, 堆等,示例来自Golang自带的Godoc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package mainimport ( "container/list" "container/ring" "fmt" ) func main () { l := list.New() e4 := l.PushBack(4 ) e1 := l.PushFront(1 ) e3 := l.InsertBefore(3 , e4) l.InsertAfter(2 , e1) for e := l.Front(); e != nil ; e = e.Next() { fmt.Println(e.Value) } l.MoveAfter(e3, e1) if l.Len() != 0 { l.Remove(l.Back()) } r := ring.New(5 ) for i := 0 ; i < r.Len(); i++ { r.Value = i r = r.Next() } r = r.Move(1 ) r.Do(func (val interface {}) { if v, ok := val.(int ); ok { fmt.Printf("value: %d \n" , v) } }) r2 := r.Unlink(2 ) r = r.Link(r2) }
container包的heap是一个抽象的堆,可以实现部分接口创建一个自定义的堆结构, 对于最大/最小堆有一些知识点顺便记录一下
二叉堆可以实现优先级队列, 堆结构是一种完全二叉树(N-1层全满, N层数据全在左侧)
堆的主要操作有上浮和下沉, 初始化堆即从第一个非叶子节点(n/2 - 1)开始下沉, 直至根节点
插入堆只需插入最后一个位置然后上浮即可
堆取顶或Pop操作, 只需调换首尾, 取尾并使根节点下沉
堆排序算法的思想就是对一个最大/最小堆不断取顶得出排序后的序列
也顺便复习一下几种树相关的概念以及差别:
最大/最小堆只要求父节点比子节点大或者小, 子节点之间没有顺序, 但二叉搜索/排序树(BST)是有顺序的
平衡的二叉搜索树有多种实现如AVL, 红黑树等, 应用如C++ STL中的set/map,
B树, B+树, B*树则是平衡多叉树, 应用如数据库索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package mainimport ( "container/heap" "fmt" ) type Item struct { value string priority int index int } type PriorityQueue []*Itemfunc (pq PriorityQueue) Len () int { return len (pq) }func (pq PriorityQueue) Less (i, j int ) bool { return pq[i].priority > pq[j].priority } func (pq PriorityQueue) Swap (i, j int ) { pq[i], pq[j] = pq[j], pq[i] pq[i].index = i pq[j].index = j } func (pq *PriorityQueue) Push (x interface {}) { n := len (*pq) item := x.(*Item) item.index = n *pq = append (*pq, item) } func (pq *PriorityQueue) Pop () interface {} { old := *pq n := len (old) item := old[n-1 ] item.index = -1 *pq = old[0 : n-1 ] return item } func (pq *PriorityQueue) update (item *Item, value string , priority int ) { item.value = value item.priority = priority heap.Fix(pq, item.index) } func main () { items := map [string ]int { "banana" : 3 , "apple" : 2 , "pear" : 4 , } pq := make (PriorityQueue, len (items)) i := 0 for value, priority := range items { pq[i] = &Item{ value: value, priority: priority, index: i, } i++ } heap.Init(&pq) item := &Item{ value: "orange" , priority: 1 , } heap.Push(&pq, item) pq.update(item, item.value, 5 ) for pq.Len() > 0 { item := heap.Pop(&pq).(*Item) fmt.Printf("%.2d:%s " , item.priority, item.value) } }