go语言学习笔记
go语言学习笔记(初级)
最近一直在学习go语言,因此打算学习的时候能够记录
一下笔记。我这个人之前是从来没有记录笔记的习惯,
一直以来都是靠强大的记忆力去把一些要点记住。
读书的时候因为一直都是有一个很安静和很专心的环境,
因此很多事情都能记得很清楚,思考的很透彻。但是随着
年纪不断增加,也算是经历了很多的事情,加上工作有时会让人
特别烦闷,很难把心好好静下来去学习,去思考大自然的终极
奥秘,因此需要记录一些东西,这些东西一方面可以作为一种自我激励
的机制,另一方面,也算是自己成长的轨迹吧。
一. 顺序编程
1. 变量
go语言变量定义的关键字是var。类型放在变量名后:
var v1 int
var v2 string var v3 [10]int //数组 var v4 []int //切片 var v5 struct{ //结构体 f int } var v6 *int //指针 var v7 map[string]int //map var v8 func(a int) int //函数
每一行不需要以分号作为结尾。 var
关键字还有一种是把多个变量的申明放在一起,
var(
v1 string
v2 int )
2. 变量初始化
有人说,变量初始化有什么好提的,那么简单。是的,但是这里面确实还是有一些值得注意的点。
var a int = 10 //完整定义 var a = 10 //自动推断是int型 a := 10 //自动推断是int型,申明并未该变量赋值
第三种初始化方式无疑是最简单的。
但是要注意的是,这里面第三种方式是和特别的,比如
var a int
a := 10
等价于
var a int
var a int a = 10
这时候就会报一个重复定义的错误。
3. 变量赋值
变量赋值和我们通常的语言基本是一致的,但是多了多重赋值功能。
i,j=j,i
这就直接实现了两个变量的交换。
4. 匿名变量
go语言的函数是多返回值的,因此可能有些值并没有被用到,这时我们就需要一个占位符去忽略这些返回值。
func GetName() (firstName, lastName, nickName string) {
return "May", "Chan", "Chibi Maruko" } _, _, nickName := GetName()
5. 定义常量
通过const关键字,可以用来定义常量。
const Pi float64 = 3.1415926 const zero = 0.0 //自动推断类型 const ( //多定义 size int64 = 10 hello = -1 ) const u , v float32 = 0.0 , 3.0 //多重赋值 const a , b , c = 1 , 2 , “hello” //自动推断类型
常量的定义也可以跟一个表达式, 但是这个表达式应该是编译的时候就可以求值的.
const mask = 1 << 3 //正常 const Home = os.GetEnv("HOME") //错误,运行时才能确定
6. 预定义常量
这里要讲一个很有意思的东西, 叫做iota.
这个东西每一个const出现的位置被置为0,没出现一个iota出现,都自增1,到写一个const出现的时候,又置为0.
const (
c1 = iota //0 c2 = iota //1 c3 = iota //2 ) const x = iota // x == 0 (因为iota又被重设为0了) const y = iota // y == 0 (同上)
如果两个const赋值表达式是一样的,可以省略后面的赋值表达式.
const (
c1 = iota //0 c2 //1 c3 //3 ) const ( a = 1 <<iota // a == 1 (iota在每个const开头被重设为0) b // b == 2 c // c == 4 )
6. 枚举
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberOfDays // 这个常量没有导出 )
大写字母开头的包外可见, 小写字母开头的包外不可见.
7. 类型
-
整型
int8, uint8, int16, uint16,int32, uint32, int64, uint64, int, uint, uintptr
不同类型不能相互比较.
-
浮点类型
float32, float64
涉及的一些运算比较简单, 我们不做细讲.
-
字符串类型
下面我们展示一个完整的go语言程序, 也是以hello
world为主题, 毕竟hello world是一个万斤油的主题.package main import "fmt" //引入依赖包 func main() { fmt.Println("hello,world!") }
这基本上是一个最简单的程序了,但是对于我们的学习非常有用,用这个模板可以写出非常好的东西出来.
字符串串本身非常简单,主要就是一些字符串操作, 比如取特定位置的字符等.
package main import "fmt" //引入依赖包 func main() { var str string = "hello,world!" fmt.Println(str) ch := str[0] //取某个特定位置的字符 fmt.Printf("%c\n",ch) length := len(str) fmt.Println(length) //len用来获取长度 str = "你好,世界" ch = str[0] fmt.Printf("%c\n",ch) length = len(str) fmt.Println(length) }
输出结果为:
hello,world! h 12 ä 13
这正好说明[]和len都不能处理中文.
字符串连接也是用+.
字符串的遍历:
package main import "fmt" //引入依赖包 func main() { var str string = "hello,world!" n := len(str) for i := 0; i < n; i++ { ch := str[i] fmt.Printf("%c\n",ch) } }
输出结果:
h e l l o , w o r l d !
对于中文, 结果是乱码, 因为是一个字节一个字节输出的, 但是默认是UTF8编码, 一个中文对应3个字节.
这里我们要提到一个range的关键字, 它可以把字符串按键值对的方式返回.package main import "fmt" //引入依赖包 func main() { var str string = "hello,world! 你好,世界!" for _, ch := range str { fmt.Printf("%c\n",ch) } }
输出结果为:
h e l l o , w o r l d ! 你 好 , 世 界 !
事实上, 字符类型有两种, 一种就是byte(uint8), 另一种是rune. 第一种遍历字符串ch是byte, 而第二种是rune.
-
数组
数组这种类型是非常常见的
[32]byte [2*N] struct { x, y int32 } [1000]*float64 [3][5]int [2][2][2]float64
数组的遍历和字符串一样,这里不再重复.
数组是值类型,在赋值时会拷贝一份.
-
数组切片
数组切片的概念比较复杂, 它有点类似于c++中vector的概念, 但又不完全一样.
我们这里详细提几点.
-
切片的创建
切片有两种创建方式, 一种是基于数组创建, 另一种是用make创建.
package main import "fmt" //引入依赖包 func main() { //从数组创建 var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10} var sa []int = myArray[5:] for _, e := range sa { fmt.Println(e) } fmt.Println(len(sa)) fmt.Println(cap(sa)) //从make创建 var mySlice2 []int = make([]int, 5, 10) for _, e := range mySlice2 { fmt.Println(e) } fmt.Println(len(mySlice2)) fmt.Println(cap(mySlice2)) //赋值 var mySlice3 []int = []int{1,2,3} for _, e := range mySlice2 { fmt.Println(e) } }
slice是引用类型.
package main import "fmt" //引入依赖包 func test(a [10]int) { a[0] = 10 } func printArray(a [10]int){ for _, e := range a { fmt.Println(e) } } func main() { var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10} printArray(myArray) test(myArray) printArray(myArray) }
输出结果:
1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
我们发现数组确实是按照值来传递. 那么如果是slice呢, 会发生什么?
package main import "fmt" //引入依赖包 func test(a []int) { a[0] = 10 } func printArray(a []int){ for _, e := range a { fmt.Println(e) } } func main() { var myArray []int = []int{1,2,3,4,5,6,7,8,9,10} printArray(myArray) test(myArray) printArray(myArray) }
输出结果:
1 2 3 4 5 6 7 8 9 10 10 2 3 4 5 6 7 8 9 10
确实是按照引用来传递的.
append函数可以往切片尾部增加元素.
mySlice = append(mySlice, 1, 2, 3)
mySlice = append(mySlice, mySlice2...)
...表示把一个slice拆成元素来处理.
package main import "fmt" //引入依赖包 func main() { var slice1 []int = make([]int,5,10) var slice2 []int = []int{1,2,3} fmt.Println(slice1) fmt.Printf("%p\n",slice1) slice1 = append(slice1,slice2...) fmt.Println(slice1) fmt.Printf("%p\n",slice1) slice1 = append(slice1,slice2...) fmt.Println(slice1) fmt.Printf("%p\n",slice1) }
输出结果:
[0 0 0 0 0] 0xc820012190 [0 0 0 0 0 1 2 3] 0xc820012190 [0 0 0 0 0 1 2 3 1 2 3] 0xc82005e000
在这里我们看到,slice的地址是所随着内内存的改变而变化的,因此是需要仔细思考的.我个人不觉得
go语言这种特性有什么好的,反正也是奇葩极了. 不过slice还提供copy, 也算是一些弥补吧.
-
-
map
go语言中,map使用非常简单.基本上看代码就会了.
package main import "fmt" //引入依赖包 //定义一个Person的结构体 type Person struct{ name string age int } func main() { var dic map[string]Person = make(map[string]Person , 100) //初始化map dic["1234"] = Person{name:"lilei",age:100} dic["12345"] = Person{name:"hanmeimei",age:20} dic["123456"] = Person{name:"dagong",age:30} fmt.Println(dic) //删除dagong delete(dic,"123456") fmt.Println(dic) //查找某个key value,ok := dic["123456"] if ok { fmt.Println(value) } value,ok = dic["1234"] if ok { fmt.Println(value) } for k,v := range dic { fmt.Println(k + ":" + v.name) } }
输出结果为:
map[12345:{hanmeimei 20} 123456:{dagong 30} 1234:{lilei 100}] map[1234:{lilei 100} 12345:{hanmeimei 20}] {lilei 100} 12345:hanmeimei 1234:lilei
map很简单吧. 数据结构我们讲完了, 接下来可以看看代码的程序控制了.
8. 程序控制
程序控制本人只提一些关键性的东西,不会啰嗦太多.
-
switch语句
switch语句不需要在每个case地下写break,默认就是执行break.如果要执行多个case, 在case最后加入fallthrough.
条件表达式不限制为常量或者整数.单个case自然可以有多个结果可以选.package main import "fmt" //引入依赖包 func test(a int) { switch { case a < 0: fmt.Println("hello") case a == 10: fallthrough case a > 10 && a < 100: fmt.Println("world") default: fmt.Println("nima") } } func main() { test(-1) test(10) test(100) }
-
循环
go语言的循环比较特别, 它用一个for就把for和while的活都干了.
package main import "fmt" //引入依赖包 func main() { sum := 0 for i := 0; i <= 100; i++ { sum += i } fmt.Println(sum) sum = 0 i := 0 for(i <= 100){ sum += i i++ } fmt.Println(sum) }
break还支持break到指定的label处.
for j := 0; j < 5; j++ { for i := 0; i < 10; i++ { if i > 5 { break JLoop } fmt.Println(i) } } JLoop: // ...
-
函数
函数是一个非常重要的概念, 也很简单. go的函数以func关键字定义, 支持不定参数和多返回值. 函数名首字符的大小写是很有讲究的:
小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。这个规则也适用于类型和变量的可见性。
package main import "fmt" //引入依赖包 //...int是不定参数,实际上就是一个slice, a,b是多返回值 func SumAndAverage(sample ...int) (a , b float64) { a , b = 0 , 0 for _, d := range sample { a += float64(d) } if len(sample) == 0 { b = 0 }else{ b = a / float64(len(sample)) } return a , b } func main() { a , b := SumAndAverage(1, 2 , 3) fmt.Println(a , b) }
很简单吧. 注意, 如果是函数里面调了其他函数, 那么这个sample怎么传给其他喊函数呢?
sample... //...表示是拆成一个个元素传递
匿名函数的概念也很简单, 只要看代码就会明白.
package main import "fmt" //引入依赖包 func main() { var myFunc func(...int)(float64, float64)= func(sample ...int) (a , b float64) { a , b = 0 , 0 for _, d := range sample { a += float64(d) } if len(sample) == 0 { b = 0 }else{ b = a / float64(len(sample)) } return a , b } a , b := myFunc(1, 2 , 3) fmt.Println(a , b) }
下面是关于闭包的概念. 这个概念在许式伟的书中被诠释的非常好:
闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,
而是在定义代码块的环境中定义。
要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定定的计算环境(作用域)。
闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在.*我们来看来两个闭包的例子.
package main import "fmt" //引入依赖包 func test(i int) func() { return func(){ fmt.Println(10+i) fmt.Printf("%p\n",&i) } } func main() { a := test(1); b := test(2) a() b() }
输出结果:
11 0xc82000a288 12 0xc82000a2c0
我们从这个结果中发现, i的地址是会变的, 因为是作为一个局部变量传进去的.
package main import "fmt" //引入依赖包 func test(x int) func(int) int { return func(y int) int { fmt.Printf("%p\n",&x) return x + y } } func main() { a := test(1); fmt.Println(a(10)) fmt.Println(a(20)) }
输出结果:
0xc82000a288 11 0xc82000a288 21
因为x只传入了一次, 因此没有改变.
package main import ( "fmt" ) func main() { var j int = 5 a := func() (func()) { var i int = 10 return func() { fmt.Printf("i, j: %d, %d\n", i, j) } }() a() j *= 2 a() }
此时输出:
i, j: 10, 5 i, j: 10, 10
二. 面向对象编程
这里我们先提值语义和引用语义的概念.
b = a
b.Mofify()
如果b改变, a不发生改变, 就是值语义. 如果b改变, a也发生改变, 就是引用语义.
go语言大多数类型都是值语义, 比如:
基本类型: byte, int, float32, float64, string
复合类型: struct, array, pointer
也有引用语义的, 比如:
slice, map, channel, interface.
这是我们要牢记的.
我们的笔记整体式按照许式伟的书来安排, 但是由于许的书提纲性很强, 内容上不是很详细, 基本上会重新整理补充一些东西进去.
-
结构体
结构体是用struct来申明的, 不多废话, 直接上代码.
package main
import (
"fmt" ) //申明一个结构体 type Person struct{ Name string Age int } func main() { //结构体的初始化方式 //1. 直接赋值 var p Person p.Name = "dingding" p.Age = 10 fmt.Println(p) //2.顺序赋值 p1 := Person{"dingding",10} fmt.Println(p1) //3. key value 赋值 p2 := Person{Name:"dingding",Age:10} fmt.Println(p2) //4.指针赋值 p3 := &Person{Name:"dingding",Age:10} fmt.Println(p3) p4 := new(Person) fmt.Println(p4) fmt.Println("---------------------------") a := p a.Name = "dongdong" b := p3 b.Name = "dongdong" fmt.Println(p) fmt.Println(p3) }
输出结果:
{dingding 10}
{dingding 10}
{dingding 10}
&{dingding 10}
&{ 0}
---------------------------
{dingding 10}
&{dongdong 10}
这说明,struct确实是值语义.
下面讨论一下结构体的组合问题. 这点许的书中并没有过多涉及, 但是还是很有必要的, 因为在实际场合中用的会很多.
package main
import (
"fmt" ) //申明一个结构体 type Human struct{ Name string Age int Phone string } //再申明一个结构体 type Employee struct { Person Human // 匿名字段Human Speciality string Phone string // 雇员的phone字段 } func main() { e := Employee{ Person:Human{ Name:"dingding", Age:11, Phone:"6666666", }, Speciality:"aaa", Phone:"77777777", } fmt.Println(e.Phone) }
这段代码看上去非常ok, 但是如果我们稍微改一下代码呢? 比如把
Person改成匿名结构, 会有些好玩的现象.
package main
import (
"fmt" ) //申明一个结构体 type Human struct{ Name string Age int Phone string } //再申明一个结构体 type Employee struct { Human // 匿名字段Human Speciality string //Phone string // 雇员的phone字段 } func main() { e := Employee{ Human{"dingding",11,"6666666"}, "aaa", //Phone:"77777777", } fmt.Println(e.Phone) }
此时输出的是6666666, 因为相当于它把Human的字段Phone继承下来了.
如果Employee里也定义Phone字段呢?
package main
import (
"fmt" ) //申明一个结构体 type Human struct{ Name string Age int Phone string } //再申明一个结构体 type Employee struct { Human // 匿名字段Human Speciality string Phone string // 雇员的phone字段 } func main() { e := Employee{ Human{"dingding",11,"6666666"}, "aaa", "77777777", } fmt.Println(e.Phone) }
此时输出的时77777777, 因为相当于是覆盖. 那么怎么输出6666666呢?
package main
import (
"fmt" ) //申明一个结构体 type Human struct{ Name string Age int Phone string } //再申明一个结构体 type Employee struct { Human // 匿名字段Human Speciality string Phone string // 雇员的phone字段 } func main() { e := Employee{ Human{"dingding",11,"6666666"}, "aaa", "77777777", } fmt.Println(e.Phone) fmt.Println(e.Human.Phone) }
输出结果:
77777777
6666666
所以, 匿名结构体的组合相当于有继承的功能.
-
为类型添加方法
这个概念和java或者是C++非常不一样, 它的理念是把似乎是把方法绑定到特定类型上去.
这个概念已经不仅仅是对象的概念了, 事实上,
我也不知道google这帮人脑子是怎么想的, 这种搓劣的复古风格,
也是让我打开眼界, 我个人觉得, go虽然仗着google的名气, 似乎显得很厉害, 但是,
比起java和C++, 简直就是个玩具, 说的不好听一点,
比起scala这样的一出生就是个大胖子, go更像是个缺胳膊少腿的畸形儿.
好了, 不说了, 直接上代码.
package main
import (
"fmt" ) //go绑定方法必须是本包内的,int不是main包内定义的. //因此需要type一下, Integer就是本包内定义的类型 type Integer int //为int绑定一个Print方法 func (i Integer) Println() { fmt.Println(i) } func main() { var a Integer = 10 a.Println() }
结果输出10, 如果是如下呢?
package main
import (
"fmt" ) //go绑定方法必须是本包内的,int不是main包内定义的. //因此需要type一下, Integer就是本包内定义的类型 type Integer int //为int绑定一个Print方法 func (i Integer) Println() { fmt.Println(i) } func main() { a := 10 a.Println() }
输出结果:
# command-line-arguments
./main.go:18: a.Println undefined (type int has no field or method Println)
因为a := 10
, go会把a推断为int, 但int并没绑上Println方法.
注意:
//为int绑定一个Print方法
func (i Integer) Println() {
fmt.Println(i)
}
这里的i
并不是引用类型,因此对i的修改不会反应到a上去.
package main
import (
"fmt" ) //go绑定方法必须是本包内的,int不是main包内定义的. //因此需要type一下, Integer就是本包内定义的类型 type Integer int //为int绑定一个Print方法 func (i Integer) Add() { i += 2 } func main() { var a Integer = 10 a.Add() fmt.Println(a) }
输出10.
如果我们用引用呢?
package main
import (
"fmt" ) //go绑定方法必须是本包内的,int不是main包内定义的. //因此需要type一下, Integer就是本包内定义的类型 type Integer int //为int绑定一个Print方法 func (this *Integer) Add() { *this += 2 } func main() { var a Integer = 10 a.Add() fmt.Println(a) }
这时输出12. 我们发现, 这个this就像是我们C++里面的this指针一样.
不过也傻13复古的多.
我们在看一个例子,
package main
import (
"fmt" ) //go绑定方法必须是本包内的,int不是main包内定义的. //因此需要type一下, Integer就是本包内定义的类型 type Integer int //为int绑定一个Print方法 func (this *Integer) Add() { fmt.Println(this) *this += 2 } func main() { var b Integer = 10 var a *Integer = &b a.Add() fmt.Println(a) }
输出结果:
0xc82000a288
0xc82000a288
我们发现,
//为int绑定一个Print方法
func (this *Integer) Add() {
fmt.Println(this)
*this += 2 }
该方法用指针调用和用值去调用, 效果是一样的. this都是指向原来对象的指针而已.
下面这个例子来自于许的书中,
package main
type Integer int func (a Integer) Less(b Integer) bool { return a < b } func (a *Integer) Add(b Integer) { *a += b } type LessAdder interface { Less(b Integer) bool Add(b Integer) } func main() { var a Integer = 1 var b LessAdder = a }
输出:
# command-line-arguments
./main.go:20: cannot use a (type Integer) as type LessAdder in assignment: Integer does not implement LessAdder (Add method has pointer receiver)
这个例子似乎有点奇怪, 为什么呢?
package main
type Integer int func (a Integer) Less(b Integer) bool { return a < b } func (a *Integer) Add(b Integer) { *a += b } type LessAdder interface { Less(b Integer) bool Add(b Integer) } type Less interface { Less(b Integer) bool } type Adder interface { Add(b Integer) } func main() { var a Integer = 1 var b Adder = a b.Add(10) }
我们可以看得更清楚:
./main.go:28: cannot use a (type Integer) as type Adder in assignment: Integer does not implement Adder (Add method has pointer receiver)
但如果是:
package main
type Integer int func (a Integer) Less(b Integer) bool { return a < b } func (a *Integer) Add(b Integer) { *a += b } type LessAdder interface { Less(b Integer) bool Add(b Integer) } type Less interface { Less(b Integer) bool } type Adder interface { Add(b Integer) } func main() { var a Integer = 1 var b Integer = a b.Add(10) }
就没有任何问题. 对比起来, 就是这两行代码:
var b Integer = a
var b Adder = a
我们接下去会娓娓道来其中的奥妙.
package main
import(
"fmt" ) //定义对象People、Teacher和Student type People struct { Name string } type Teacher struct{ People Department string } type Student struct{ People School string } //对象方法实现 func (p *People) SayHi() { fmt.Printf("Hi, I'm %s. Nice to meet you!\n",p.Name) } func (t *Teacher) SayHi(){ fmt.Printf("Hi, my name is %s. I'm working in %s .\n", t.Name, t.Department) } func (s *Student) SayHi() { fmt.Printf("Hi, my name is %s. I'm studying in %s.\n", s.Name, s.School) } func (s *Student) Study() { fmt.Printf("I'm learning Golang in %s.\n", s.School) } //定义接口Speaker和Learner type Speaker interface{ SayHi() } type Learner interface{ SayHi() Study() } func main() { //初始化对象 people := People{"张三"} // teacher := &Teacher{People{"郑智"}, "Computer Science"} // student := &Student{People{"李明"}, "Yale University"} var speaker Speaker //定义Speaker接口类型的变量 speaker = people speaker.SayHi() }
这时就会出现上面我们提到的错误. 尽管如果我们这么去调用:
package main
import(
"fmt" ) //定义对象People、Teacher和Student type People struct { Name string } type Teacher struct{ People Department string } type Student struct{ People School string } //对象方法实现 func (p *People) SayHi() { fmt.Printf("Hi, I'm %s. Nice to meet you!\n",p.Name) } func (t *Teacher) SayHi(){ fmt.Printf("Hi, my name is %s. I'm working in %s .\n", t.Name, t.Department) } func (s *Student) SayHi() { fmt.Printf("Hi, my name is %s. I'm studying in %s.\n", s.Name, s.School) } func (s *Student) Study() { fmt.Printf("I'm learning Golang in %s.\n", s.School) } //定义接口Speaker和Learner type Speaker interface{ SayHi() } type Learner interface{ SayHi() Study() } func main() { //初始化对象 people := People{"张三"} // teacher := &Teacher{People{"郑智"}, "Computer Science"} // student := &Student{People{"李明"}, "Yale University"} //var speacker Speaker //定义Speaker接口类型的变量 //speacker = people people.SayHi() }
或者
package main
import(
"fmt" ) //定义对象People、Teacher和Student type People struct { Name string } type Teacher struct{ People Department string } type Student struct{ People School string } //对象方法实现 func (p *People) SayHi() { fmt.Printf("Hi, I'm %s. Nice to meet you!\n",p.Name) } func (t *Teacher) SayHi(){ fmt.Printf("Hi, my name is %s. I'm working in %s .\n", t.Name, t.Department) } func (s *Student) SayHi() { fmt.Printf("Hi, my name is %s. I'm studying in %s.\n", s.Name, s.School) } func (s *Student) Study() { fmt.Printf("I'm learning Golang in %s.\n", s.School) } //定义接口Speaker和Learner type Speaker interface{ SayHi() } type Learner interface{ SayHi() Study() } func main() { //初始化对象 people := People{"张三"} // teacher := &Teacher{People{"郑智"}, "Computer Science"} // student := &Student{People{"李明"}, "Yale University"} var speacker Speaker //定义Speaker接口类型的变量 speacker = &people speacker.SayHi() }
这样就都没有任何问题, 这就是说什么呢? 这说明对于对象的方法, 无论接受者是对象还是对象指针, 都没
任何问题. 但是如果是借口,如果接口中存在某个方法,绑定的接收者是对象指针,那么这个接口
也只能被该对象指针赋值. 如此奇葩的设计, 我只能说, go的设计者真是个脑残.
-
继承
好了, 下面正式讲继承的语法, 话说那玩意儿的真的算不上继承, 比C++的继承真的时不知道low到哪里去了. 但是我也不知道为啥这是go爱好者们最爱标榜的东西,
有时我想想, 程序员也真是单纯的人, 一点点的蛊惑, 就会让他们激动不已,
感觉就要去参加革命了似的.
go的继承非常简陋, 就是一个匿名结构组合的问题. 不废话,直接上代码.
package main
import(
"fmt" ) //基类 type Base struct{ Name string } //绑定Say方法 func (b *Base) Say() { fmt.Println(b.Name) } //绑定ok方法 func (b *Base) Ok() { fmt.Println("ok") } //Foo有个匿名结构Base type Foo struct{ Base Name string } //重写Say方法 func (f *Foo) Say() { f.Base.Say() fmt.Println(f.Name) } func main() { var f *Foo = &Foo{Base{"father"},"sun"} f.Ok() f.Say() }
输出结果:
ok
father
sun
ok,下面我们看看多继承二义性的问题.
package main
import(
"fmt" ) //father type Father struct{ } func (f *Father)Say() { fmt.Println("father") } //mother type Mother struct{ } func (f *Mother)Say() { fmt.Println("mother") } //sun type Sun struct{ Father Mother } func main() { var s *Sun = new(Sun) s.Say() }
输出:
# command-line-arguments
./main.go:32: ambiguous selector s.Say
果然展现了二义性. 消歧义方式也是土的掉渣:
package main
import(
"fmt" ) //father type Father struct{ } func (f *Father)Say() { fmt.Println("father") } //mother type Mother struct{ } func (f *Mother)Say() { fmt.Println("mother") } //sun type Sun struct{ Father Mother } func main() { var s *Sun = new(Sun) s.Father.Say() s.Mother.Say() }
我也是醉了.
此外, 我们也会看到还有一种用法,
package main
import(
"fmt" ) //基类 type Base struct{ Name string } //绑定Say方法 func (b *Base) Say() { fmt.Println(b.Name) } //绑定ok方法 func (b *Base) Ok() { fmt.Println("ok") } //Foo有个匿名结构Base type Foo struct{ *Base //base是个指针 Name string } //重写Say方法 func (f *Foo) Say() { f.Base.Say() fmt.Println(f.Name) } func main() { var f *Foo = &Foo{&Base{"father"},"sun"} f.Ok() f.Say() }
这里Foo的Base是一个指针类型, 因此我们传入的也必须是个指针, 我们可以看到这种用法.
-
接口
这个我不想吐槽什么, 前面已经吐槽过了, 但是这种设计, 确实也是诡异之极.
go的借口是非侵入式的, 只要实现了接口定义的方法, 就等于实现了该接口. 换句话说, 接口的实现和定义式可以分离
不相关的.
接口的例子还是之前那个:
package main
import(
"fmt" ) type Integer int func (a Integer) Less(b Integer) bool { return a < b } func (a *Integer) Add(b Integer) { *a += b } //定义接口 type LessAdder interface { Less(b Integer) bool //函数声明 Add(b Integer) //函数声明 } func main() { var a Integer = 10 var b LessAdder = &a //道理我们前面提到过了,Add接收者是个对象指针 fmt.Println(b.Less(5)) b.Add(20) fmt.Println(a) }
输出:
false
30
只要两个接口拥
有相同的方法列表(次序不同不要紧),那么它们就是等同的,可以相互赋值。
接口赋值并不要求两个接口必须等价。如果接口A的方法列表是接口B的方法列表的子集,
那么接口B可以赋值给接口A。
几个接口也可以组合出一个接口.
type ReadWriter interface {
Reader
Writer
}
等价于:
type ReadWriter interface {
Read(p []byte) (n int, err error) Write(p []byte) (n int, err error) }
-
Any类型
由于Go语言中任何对象实例都满足接口interface{},所以interface{}看起来是任何对象的Any类型
var v1 interface{} = 1 var v2 interface{} = "abc" var v3 interface{} = &v2 var v4 interface{} = struct{ X int }{1} var v5 interface{} = &struct{ X int }{1} func Printf(fmt string, args ...interface{}) func Println(args ...interface{})
-
接口转换和类型查询
接口转换
package main
import(
"fmt" ) type Integer int func (a Integer) Less(b Integer) bool { return a < b } func (a *Integer) Add(b Integer) { *a += b } //定义接口 type LessAdder interface { Less(b Integer) bool //函数声明 Add(b Integer) //函数声明 } //定义接口 type Adder interface { Add(b Integer) //函数声明 } func main() { var a Integer = 10 var b LessAdder = &a //道理我们前面提到过了,Add接收者是个对象指针 if c , ok = b.(Adder); ok{ c.Add(10) fmt.Println(a) //fmt.Println(c.Less(100)) //报错,c.Less undefined (type Adder has no field or method Less) } }
类型查询
package main
import(
"fmt" ) func main() { b := "a" var a interface{} = b switch a.(type) { case int: fmt.Println("int") case float32: fmt.Println("float32") case int32: fmt.Println("int32") case float64: fmt.Println("float64") case bool: fmt.Println("bool") case string: fmt.Println("string") default: fmt.Println("ok") } }
-
补充
我们补充一些defer-panic-recover的案列.
package main
import (
"fmt" ) func f() { fmt.Println("a") } func main() { defer func() { if err := recover(); err != nil{ fmt.Println(err) } }() f() fmt.Println("b") }
输出结果:
a
b
如果我们在f中panic呢? 这会发生什么呢?
package main
import (
"fmt" ) func f() { fmt.Println("a") panic("error!") } func main() { defer func() { if err := recover(); err != nil{ fmt.Println(err) } }() f() fmt.Println("b") }
输出:
a
error!
我们发现, b没有输出. panic会抛出一个异常, 由recover去捕获.f抛出异常后, 事实上,
main剩下的部分都不会执行, 但是因为我们defer了,
defer是一定会执行的,因此我们在defer中捕获了panic抛出的
异常. 这就是为什么b没有输出. 似乎和try catch很像. 如果我们希望把b也输出, 但也能捕获异常呢?
package main
import (
"fmt" ) func f() { fmt.Println("a") panic("error!") } func main() { func(){ defer func() { if err := recover(); err != nil{ fmt.Println(err) } }() f() }() fmt.Println("b") }
输出结果:
a
error!
b
如果是这样呢?
package main
import (
"fmt" ) func f() { fmt.Println("a") panic("error!") } func main() { func(){ f() defer func() { if err := recover(); err != nil{ fmt.Println(err) } }() }() fmt.Println("b") }
此时, 在定义defer之前, f已经panic了, 没有recover去捕获, 这个panic会一直抛出.
直到被go虚拟机捕获.
输出:
a
panic: error!
goroutine 1 [running]:
main.f()
/Users/fengyan/code/go/test/main.go:9 +0x11e main.main.func1() /Users/fengyan/code/go/test/main.go:14 +0x18 main.main() /Users/fe
go里面有个东西很好玩, nil类似于java的null, 那么java如果对null调用方法, 会直接抛出一个空指针异常.
那么go会怎么样呢?
package main
func main() {
nil.Println("a") }
输出结果:
# command-line-arguments
./main.go:4: use of untyped nil
看来还是不行的.
所以调用前我们还是要进行空的判断.
三. go并发编程
并发不是并行. 并发比并行更加优秀. 并发是时间片轮换, 并行是多核计算. 事实上, 并行可以由并发指定到多个cpu执行.
我们马上看一个具体的例子.
package main
import (
"fmt" ) func Add(x, y int) { z := x + y fmt.Println(z) } func main() { for i := 0; i < 10; i++ { go Add(i,i) } }
结果我们发现什么都没有输出, 这是因为go是异步执行的, main不会等Add完成, 它会继续执行下去, 于是发生了main
函数先结束, 于是进程就结束了.
所以这里我们需要做同步, 所谓同步, 就是主线程去等待子线程完成(go里的线程其实是协程, 协程你可以认为是轻量级的线程).
线程间通信模型基本上常用的也就是两种,共享内存和消息队列.
后者目前看来比较流行, 比如akka(actor模型), zmq(消息队列模型)等 都是基于消息队列的并发模型.
go也是采用了消息队列的方式, 这里就是channel.
channel的申明形式:
var chanName chan ElementType
ElementType是该chan能够支持的数据类型.
chan的初始化有两种:
ch := make(chan int)
和
ch := make(chan int,capacity)
前者写入和读取是阻塞的, 后者自带了一个buffer,
如果没有达到capacity, 是非阻塞的, 达到capacity才会阻塞. 读取的话, 如果为buffer空,
会阻塞.
chan写入数据
ch <- value
chan读取数据
value : = <-ch
对于带buffer的chan也可以用for和range读取:
for i := range ch {
fmt.Println("Received:", i) }
下面我们把同步后的代码贴上:
package main
import (
"fmt" ) func Add(x, y int, ch chan int) { z := x + y fmt.Println(z) //写入后就阻塞 ch <- 1 } func main() { //chan int slice, make([]chan int, 10)是创建slice的方法 //容量是10,超过这个容量,会发生内存拷贝 //var chs []chan int = make([]chan int, 10) //其实这里用数组更好 var chs [10]chan int for i := 0; i < 10; i++ { chs[i] = make(chan int) go Add(i,i,chs[i]) } //同步 for _, ch := range chs{ //如果线程没有写入数据, 会阻塞 <- ch } }
输出结果:
18
0
2
4
12
14
16
10
8
6
下面我们介绍一下select语法,
select {
case <-chan1:
//读取数据 case chan2 <- 1: //写入数据 default: //默认操作 }
select会去注册事件, 比如是否可读,是否可写, 根据对应的事件去执行相应的代码.
package main
import (
"fmt" ) func RandomSignal(ch chan int) { for{ select{ //写入1事件 case ch <- 1: //写入0事件 case ch <- 0: } } } func main() { //var ch chan int = make(chan int)效果一样 //但是有些微妙的不同 var ch chan int = make(chan int,1) go RandomSignal(ch) for value := range ch{ fmt.Println(value) } }
这是一个随机产生0或者1的信号发生器.
单向channel
顾名思义,单向channel只能用于发送或者接收数据。channel本身必然是同时支持读写的,
否则根本没法用。假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数
据。同理,如果一个channel只允许写,即使写进去了,也没有意义,因为没有机会读取里面
的数据。所以的单向channel概念,其实只是对channel的一种使用限制。
var ch1 chan int // 双向 var ch2 chan<- float64// 只写 var ch3 <-chan int // 只读
那么单向channel如何初始化呢?之前我们已经提到过,channel是一个原生类型,因此不仅
支持被传递,还支持类型转换。只有在有了单向channel的概念后,读者才会明白类型转换对于
channel的意义:就是在单向channel和Ԥ向channel之间进行转换。示例如下:
ch4 := make(chan int) ch5 := <-chan int(ch4) // 只读 ch6 := chan<- int(ch4) //只写
关闭channel
close(ch)
如何判断一个channel是否已经被关
闭?我们可以在读取的时候使用多重返回值的方式:
x, ok := <-ch
package main
import (
"fmt" ) func RandomSignal(ch chan int) { for i:= 0; i <= 9; i++{ select{ //写入1事件 case ch <- 1: //写入0事件 case ch <- 0: } } close(ch) } func main() { //var ch chan int = make(chan int)效果一样 //但是有些微妙的不同 var ch chan int = make(chan int,1) go RandomSignal(ch) for value := range ch{ fmt.Println(value) } }
输出:
1
0
1
1
0
1
1
0
0
0
close(ch)之后, 对ch的for循环会终止.
并行计算:
package main
import (
"runtime" "sync" "fmt" ) type Vector []float64 func min(a int, b int) int { if a < b { return a } return b } func max(a int, b int) int { if a < b { return b } return a } func mul(u, v Vector, k int) (res float64) { n := min(k+1, len(u)) j := min(k, len(v)-1) for i := k - j; i < n; i, j = i+1, j-1 { res += u[i] * v[j] } return } func Convolve(u, v Vector) (w Vector) { n := len(u) + len(v) - 1 w = make(Vector, n) size := max(1, 1<<20/n) wg := new(sync.WaitGroup) wg.Add(1 + (n-1)/size) for i := 0; i < n && i >= 0; i += size { j := i + size if j > n || j < 0 { j = n } go func(i, j int) { for k := i; k < j; k++ { w[k] = mul(u, v, k) } wg.Done() }(i, j) } wg.Wait() return } func main() { //numcpu := runtime.NumCPU() numcpu := 2 runtime.GOMAXPROCS(numcpu) //设置使用多少个cpu核心 var a Vector = make([]float64,1000000) var b Vector = make([]float64,1000000) for i := 0; i < 1000000 - 1; i++ { a[i] = 1 b[i] = 1 } w := Convolve(a,b) fmt.Println(w) }
runtime.GOMAXPROCS(numcpu)
该函数是用来设置cpu使用的核心个数, 在许的书中, 如果不设置这个环境变量, 默认是1个核心, 但是事实上,
在我这个版本的go语言中, 已经支持默认多核并发啦.
下面我们来看看同步的概念.
Go语言包中的sync包提供了两种ᩙ类型:
sync.Mutex和sync.RWMutex。sync.Mutex
是读写都锁, sync.RWMutex是写锁读不锁.
用法:
var l sync.Mutex
func foo() {
l.Lock()
defer l.Unlock() //... }
对于从全局的角度只需要运行一次的代码,比如全局初始化操作,Go语言提供了一个Once
类型来保证全局的唯一性操作,具体代码如下:
var a string
var once sync.Once func setup() { a = "hello, world" } func doprint() { once.Do(setup) print(a) } func twoprint() { go doprint() go doprint() }
如果不考虑线程安全, 等价于:
var done bool = false //全局变量 func setup() { a = "hello, world" done = true } func doprint() { if !done { setup() } print(a) }
WaitGroup
var wg sync.WaitGroup
该类型有三个指针方法,即Add、Done和Wait。
类型sync.WaitGroup是一个结构体类型。在它之中有一个代表计数的字段。
当一个sync.WaitGroup类型的变量被声明之后,其值中的那个计数值将会是0。
我们可以通过该值的Add方法增大或减少其中的计数值。
虽然Add方法接受一个int类型的值,并且我们也可以通过该方法减少计数值,但是我们一定不要让计数值变为负数。因为这样会立即引发一个运行恐慌。
这也代表着我们对sync.WaitGroup类型值的错误使用。
除了调用sync.WaitGroup类型值的Add方法并传入一个负数之外,我们还可以通过调用该值的Done来使其中的计数值减一。
当我们调用sync.WaitGroup类型值的Wait方法的时候,它会去检查该值中的计数值。
如果这个计数值为0,那么该方法会立即返回,且不会对程序的运行产生任何影响。 但是,如果这个计数值大于0,
那么该方法的调用方所属的那个Goroutine就会被阻塞。
直到该计数值重新变为0之时,为此而被阻塞的所有Goroutine才会被唤醒。
我们来看第一个例子:
package main
import (
"fmt" "sync" ) func Add(x, y int, wg *sync.WaitGroup) { z := x + y fmt.Println(z) wg.Done() } func main() { var wg *sync.WaitGroup = &sync.WaitGroup{} wg.Add(10) for i := 0; i < 10; i++ { go Add(i,i,wg) } wg.Wait() }
初级教程我们就这样快速结束了, 咱们高级见. 嗯, 233333333.