Go学习笔记
2017年12月30日,我学习Go的第一天,我打算用不到一个月的时间,学习Go语法基础,Go实战,利用Go构建微服务,Go的Web编程,。
学习资料:
1、环境配置
Go最重要的是要配置GOROOT和GOPATH。对于前者,如果简易安装,自动会设置该变量;对于后者,我觉得这个和python的PYTHONPATH差不多,定义了搜索模块的路径。
2、基本思想
今天学习Go,发现该语言和Python有很大不同,不是像Python有虚拟机直接编译运行,而是继承了C语言的思想,需要先进行编译链接等过程。比如我在python中,我想运行一个模块,直接python命令执行即可。而Go语言和C一样,对于需要调用的模块,首先要将其编译编程".a"文件,主文件要生成二进制可执行文件之后执行。
上面说到了主文件,这也是和Python不同,Go需要定义一个主入口文件,即需要定义main入口函数。说实话啊,我当初学C语言的时候,就觉得挺不灵活了,而Python的结构却非常灵活,谁都可以是主模块,当然了,你也可以说它结构不清晰。
之前也大致看了看别人写的Go的优缺点,简单说,就是它集百家之精华,弃百家之糟粕。后续会好好研究。
3、go install和go build
go install/build都是用来编译包和其依赖的包的吧,不同的是,go install一般生成静态库文件放在$GOPATH/pkg目录下,文件扩展名a,如果为main包,则会在$GOPATH/bin 生成一个可执行的二进制文件。go build好像只对main包有效,在当前目录编译生成一个可执行的二进制文件(依赖包生成的静态库文件放在$GOPATH/pkg) .
4、包
Go的包和Python的很类似,我们可以调用包中的模块。不同的是,对于一个包的定义,Python是通过加入在对应目录加入__init__,Go是在go文件中定义“package name”。
语法相关
1. 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )
进行赋值操作时要注意,这和Python有很大的不同。Python中无论是什么类型,a = b都是复制了b的引用,然而Go中却不同,对于 int、float、bool 和 string ,array这些基本类型 ,a = b复制的是b指向的内存中的值,赋值后,a和b完全无任何关系。切片,map等数据结构是赋值的指针引用。在Go中,就是分成了值类型和引用类型两大类。
字符串尽量用双引号和反引号,而不用单引号(rune类型)。
2.数组和切片
Go的数组和python的区别还是挺大的,或者说Python作为动态型语言,和别的语言都很有区别。Go的数组和C语言一样,定义时需要指定长度,且数组中的元素必须是同一类型。数组没有append等参数,在初始化的时候,如果不赋值,都会被赋上一个默认值,说实话,数组的确不是特别灵活。但Go同样还有slice结构来满足动态数组的需求。它和数组的最大区别是数组是静态的,不可扩展的,slice是可扩展的,动态变化的。且进行复制的时候,数组是赋值值,slice是赋值指针,这点和Python的引用比较类似。
func main() {
arr := [5]int{1,2,3,5}
//change the array's value
fmt.Println(arr,len(arr))
//arr = append(arr,13)
slice := arr[1:4]
arr[3] = 4
arr2 := arr
arr2[0] = 20
slice[1] = 8
slice = append(slice,0,6,7)
fmt.Println(arr,len(arr))
fmt.Println(arr2,len(arr2))
fmt.Printf("the slice is %v,%d,max len:%d\n",slice,len(slice),cap(slice))
}
3、控制,循环语句
Go的if语句和Python一样,if判断不需要加括号,但是代码块需要用花括号,不是以缩进控制的,这和C语言等其他语言一样。此外Go没有elif,只能用else if。但Go有一个比较强大的地方是,他可以在if语句上声明变量。但这个变量只存在于这个逻辑块中。例子:
if x := 3;x>2 {
fmt.Println(x)
} else {
fmt.Println(2)
}
如果你觉得if 语句麻烦,可以使用switch语句,Python中没有switch。
Go中的循环是for实现,没有while和util等。但for可以实现while的功能。直接拿例子把:
i := 1
for i<10 {
if i%2 == 0 {
i++
continue
} else {
fmt.Println(i)
i += 1
}
}
上面是实现while的功能。基本的for循环就是第一表达式是循环前调用的,第二个表达式是循环判断条件,第三个是每次循环结束后调用的。
4、range
Go中也有range,但他和Python中的range完全不是一回事,就是名相同罢了。Go的range主要用来for循环中的迭代。
当用于遍历数组和切片的时候,range函数返回索引和元素;
当用于遍历字典的时候,range函数返回字典的键和值。
range也可以迭代字符串,但除了第一个索引外,第二个返回的是字符的对应的字节值。
for i,v := range map[string]int{"haibo":29,"lina":31} {
fmt.Println(i,v)
}
for i,v := range "haibo" {
fmt.Println(i,v)
}
output:
haibo 29
lina 31
0 104
1 97
2 105
3 98
4 111
这还要注意一下,看下面这两种用法:
s := []int{1,2,5}
for _,c := range s {
c = 3
}
for i,_ := range s {
s[i] = 3
}
这两者是完全不相同的,第一个不会改变原先的s数组的元素,第二个会改变。
import
Go的import,如果你import了,但是没使用,语法上是不能通过的。
函数
除了需要定义参数和返回值的类型之外,Go语言的函数和Python还有很多的不同。在Python中,我们都知道传递给函数的是复制了形参的引用,至于最后是传值和传址,结果是根据参数的数据类型来决定的。在Go中,有很大的不同的。Go的参数默认是值传递,即传入的是形参的值的copy。当我们在函数内部改变其值后,并不能影响函数外的变量。
func cal(a int,b int) (int,int) {
a = a+b
b = a*b
return a,b
}
func charray(arr [4]int) {
arr[0] = 100
fmt.Println(arr)
}
func main() {
arr := [4]int{1,2,3,5}
x,y := 3,4
a,b := cal(x,y)
fmt.Printf("a=%d,b=%d\n",a,b)
fmt.Printf("x=%d,y=%d\n",x,y)
charray(arr)
fmt.Println(arr)
}
a=7,b=28
x=3,y=4
[100 2 3 5]
[1 2 3 5]
看到了吧,上面传入的都是value的拷贝。除了传值,也可以传引用。Go的slice和数组不同,它传入的是引用。
func charray(arr []int) {
arr[0] = 100
fmt.Println(arr)
}
func main() {
arr := []int{1,2,3,5}
charray(arr)
fmt.Println(arr)
}
[100 2 3 5]
[100 2 3 5]
当然,对于其他数据类型,可以传入内存地址来实现传引用。
func cal(a *int,b *int) (int,int) {
*a = *a+*b
*b = *a * *b
return *a,*b
}
func main() {
x,y := 3,4
a,b := cal(&x,&y)
fmt.Printf("a=%d,b=%d\n",a,b)
fmt.Printf("x=%d,y=%d\n",x,y)
}
a=7,b=28
x=7,y=28
说完了上面关于参数的问题,再说一下Go中的闭包。闭包其实和其他语言,如Python语言的闭包是相同的,至少是在思想上是一致的,只是在语法上不同罢了。下面是实现斐波那契的例子:
package main
import "fmt"
// fibonacci 函数会返回一个返回 int 的函数。
func fibonacci() func() int {
a,b := 0,1
return func() int {
a, b = b, a+b
return b
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
a,b作为外部的局部变量。内嵌函数结束后声明周期也没结束,而是一直存活在内存中。所以才会依次调用后,它的值是递增的。
make和new
两者都是进行内存分配的函数,但两者有很大的区别。
new是为值类型分配内存,成功后会返回一个内存块的指针,且初始化为0。
make是为引用类型分配内存,返回的不是指针,而是引用。
方法method.
如果接触多了Python和Java等利用类实现的面向对象,乍一看Go,的确是有点别扭的。我自认为,Go被称作是21世纪的C语言,那就应该是在面向过程的基础上实现了面向对象的一些功能。
Go的method的定义也像定义普通函数一样,但最大的区别是你要定义个Receiver,即你希望谁拥有这个方法。下面是个例子:
type Node struct {
height,weight int
}
func (n Node) area() int {
n.height = 10
return n.height * n.weight
}
func (n *Node) area() int {
n.height = 10
return n.height * n.weight
}
我上面定义了一个结构体Node,下面定义一个area,它的接收者就是Node。也就是说Node此时拥有了area的方法。
node := Node{5,6}
fmt.Println("the area is:",node.area())
通过上面的方法,你可以访问area就像访问Node自己的属性一样。不过呢,就我目前学这几天来看啊,我是觉得有点不方面。不过听他们说Go的精髓是interface,method也是配合interface的,所以接下来好好学学interface。
现在,我们再看一下上面的代码,我通过两种方式定义了area,一个是指针,一个是普通的struct,这两者是有区别的。 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。
如果用指针定义的,那么上面的输出结果是:
the area is: 60 {10 6}
如果是普通struct类型定义的,那么上面的输出结果是:
the area is: 60 {5 6}
从结果可以看出,普通类型的你只是以副本的形式操作对象,对原node没有任何改变。
根据书上的例子,写了下面的代码,我觉得他囊括了很多方面的知识,比较好。
下面的知识点包括 :
- type的用法;
- method的用法(普通类新和指针类型);
- 数组;
- range的用法;
- for 循环的用法;
- iota的用法;
package main
import "fmt"
const (
WHITE = iota
BLACK
BLUE
RED
)
type Color uint8
type Box struct {
height,width,depth float64
color Color
}
func (b Box) Volume() float64 {
return b.height * b.width * b.depth
}
func (b *Box) SetColor(c Color) {
b.color = c
}
func (c Color) String() string {
colors := []string{"WHITE","BLACK","BLUE","RED"}
return colors[c]
}
type BoxList []Box
func (bL BoxList) BiggestColor() Color {
largest_volume := 0.0
var b Box
for i,_ := range bL {
if bL[i].Volume() > largest_volume {
largest_volume = bL[i].Volume()
b = bL[i]
}
}
return b.color
}
func (bl BoxList) PaintItBlack() {
for i,_ := range bl {
bl[i].SetColor(WHITE)
}
}
func main() {
box := Box{3,4,5,BLUE}
fmt.Println(box.height,box.width,box.depth,box.color)
fmt.Println(box.Volume())
box.SetColor(BLACK)
fmt.Println(box)
box2 := Box{4,2,80,RED}
boxlist := BoxList{box,box2}
fmt.Println(boxlist.BiggestColor())
boxlist.PaintItBlack()
for _,b := range boxlist {
fmt.Println(b.color)
}
fmt.Println(s)
}
接口interface
之前在学Python的时候,接触了一个概念,叫鸭子类型。对于面向对象,应该还好理解,也好实现。即我定义了一个基类animal,该类实现了诸如call,run方法,然后我又衍生出几个子类,如dog,pig,lion等等,每个子类又重构了基类的方法。那在其他地方就可以调用所有实现call,run的子类。虽然是不同的子类,但我们都实现了相同的方法,这就是著名的鸭子类型,即我不关心你怎么实现的,我不关心你是狗,还是猫,你只要能叫,你就是鸭子。
在Go中,我们也可以实现上述说的功能,即利用接口。接口就是一系列的method的组合,只要某一个对象满足了接口中的所有方法,那我们就说这个对象是interface类型。
例子:
package main
import "fmt"
type animal interface {
Call()
Run() string
}
type Duck struct {
name string
}
func (d Duck) Call() {
fmt.Println("I'm ",d.name)
}
func (d Duck) Run() string {
fmt.Printf("%s can run\n",d.name)
return "can"
}
func IsAnimal(a animal){
fmt.Println("yeah,I'm a animal")
}
func main() {
d := Duck{"dog"}
d.Call()
fmt.Println(d.Run())
IsAnimal(d)
}
看上面的dog结构体,我虽然没显示地初始化它为interface,但我在执行IsAinmal这个函数时,也没有报错。因为它实现了interface的方法集合。
Go作为一种静态语言,数据类型需要事先声明的,如果你想给一个整数,赋值一个字符串,那是不可以的。而Go提供了一种方法,即interface可以实现Python那样的动态。如:
type empt interface {}
var e empt
i := 1
s := "haibo"
e = i
e = s
上面是一个不含任何方法的接口,这也意味着所有的数据类型都实现了这个接口,这个接口可以存储任何类型的值。
接着再拿一个例子,进一步说明空的接口,同时也介绍一个类型断言。
type Empty interface {}
type List []Empty
func main() {
stu := Student{Human{20,"haibo"},"beijiaoda"}
var p People
p = stu
fmt.Println(p.Talk())
p.Think()
if _,ok := p.(Student);ok{
fmt.Printf("ok,I'm Student type\n")
}
i,str,student := 1,"hello",stu
list := List{i,str,student}
for index,item := range list {
switch value := item.(type) {
case int :
fmt.Printf("in index:%d is int,value:%d\n",index,value)
case string :
fmt.Printf("in index:%d is string,value is %s\n",index,value)
case Student :
fmt.Printf("in index:%d is student,his name is %v\n,%v",index,value.name)
default:
fmt.Println("ok,it is uknown type")
}
}
}
断言两种形式:
value, ok := element.(T) value是获得的对应的值, value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型 ;
value := element.(type) 直接得到对应的变量类型。
注意:只有接口和存储的类型和对象都是nil时,接口才会是nil。
反射
package main
import "fmt"
import "reflect"
type People interface {
Talk() string
Think()
}
type Human struct {
age int
name string
}
type Student struct {
Human
school string
}
func (s Student) Talk() string {
return fmt.Sprintf("my name is %v,I'm a student from %v\n",s.name,s.school)
}
func (s Student) Think() {
fmt.Printf("%v is thinking \n",s.name)
}
func Info(o interface{}) {
t := reflect.TypeOf(o)
v := reflect.ValueOf(o)
fmt.Println(t,v)
}
func main() {
stu := Student{Human{20,"haibo"},"beijiaoda"}
var p People
p = stu
Info(p)
}
我觉得反省可以用来判断对象类型,类似Python的自省函数,type.
Go的并发
原理有点像Python中的yield,即由用户设定中断。简单例子
package main
import "fmt"
import "runtime"
//import "time"
func news(s string){
for i:= 1;i<5;i++ {
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go news("hehe")
news("haha")
}
延迟加载
在学Python的时候,有一个延迟加载的概念。我就拿一个例子说:
def Defer():
return [lambda :i*i for i in range(1,4)]
for f in Defer():
print f()
一般通过给内嵌函数传递一个值拷贝,从而避免延迟加载:
def g():
return [lambda j=i:j for i in range(4)]
在Go中同样也有,它是通过defer和go语句实现的。
package main
import "fmt"
import "time"
func main() {
inter := [...]int{1,2,3,5}
for _,v := range inter {
go func() {
fmt.Println(v)
}()
//time.Sleep(time.Millisecond)
}
time.Sleep(time.Millisecond)
}
go语句是生成新的goroutine,for结束之后才会执行go语句。
package main
import "fmt"
func main() {
inter := [...]int{1,2,3,5}
for _,v := range inter {
defer func(i int) {
fmt.Println(i)
}(v)
}
}
defer语句也是一样,但defer有一点不同的是,它是逆序的。
下面看一个defer,recover和panic结合使用的一个例子:
func main() {
defer func() {
if err := recover();err != nil {
fmt.Println("I'm defer with recover")
} else {
fmt.Println("no error")
}
}()
defer func() {
if err := recover();err != nil {
fmt.Println("second defer")
}
}()
panic("raise one error")
fmt.Println("main goroutine ends normally")
}
它的输出是:
second defer
no error
当遇到panic的时候,肯定要执行defer函数的,但defer函数是要遵循逆序的顺序的。我在这里定义了两个defer函数,首先执行第二个,然后执行第一个。第二个执行之后执行第一个defer。你看这个输出的结果应该可以发现:首先执行的defer有recover,那么等执行下一个defer的时候,就没有panic传递的error了,此时变得正常了。这就是recover的作用。它不会导致程序马上挂掉。
但是呢,虽然recover的存在可以不会导致程序立马挂掉,但是呢,在defer执行完毕之后,函数也会立即返回。
说到这里,应该发现。Go的defer有点像Python里面捕获异常的finally。即:
try:
except:
finally:.
无论是否有错误,最后都要执行的。
微信分享/微信扫码阅读