Go学习笔记

2017年12月30日,我学习Go的第一天,我打算用不到一个月的时间,学习Go语法基础,Go实战,利用Go构建微服务,Go的Web编程,。

学习资料:

  1. Go Web编程
  2. Go官方文档
  3. Go中文社区
  4. Go基础教程
  5. Go开源项目

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:.

无论是否有错误,最后都要执行的。

--------EOF---------
微信分享/微信扫码阅读