F's Blog

博客 收藏夹
Go 语言

07 Dec 2017

语法

Go 只有 25 个关键字:

break    default      func    interface    select
case     defer        go      map          struct
chan     else         goto    package      switch
const    fallthrough  if      range        type
continue for          import  return       var

其中也就 defer, go, chan, package, fallthrough, range 和 var 需要解释一下。

string

Go 里字符串是不可以改变的,如果需要,可以用字符数组:

s := "hello"
c := []byte(s)  // 将字符串 s 转换为 []byte 类型
c[0] = 'c'
s2 := string(c)  // 再转换回 string 类型
fmt.Printf("%s\n", s2)

反引号括起来的是 raw 字符串,如同 HEREDOC:

s = `Hello
World!`

内置函数

make

Panic

是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数 F 调用 panic,函数 F 的执行被中断,但是F中的延迟函数会正常执行,然后 F 返回到调用它的地方。在调用的地方,F 的行为就像调用了panic。这一过程继续向上,直到发生 panic的 goroutine 中所有调用的函数返回,此时程序退出。恐慌可以直接调用 panic 产生。也可以由运行时错误产生,例如访问越界的数组。

下面这个函数演示了如何在过程中使用panic:

var user = os.Getenv("USER")

func init() {
	if user == "" {
		panic("no value for $USER")
	}
}

Recover

是一个内建的函数,可以让进入令人恐慌的流程中的 goroutine 恢复过来。recover 仅在延迟函数中有效。在正常的执行过程中,调用 recover 会返回 nil,并且没有其它任何效果。如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

下面这个函数检查作为其参数的函数在执行时是否会产生panic:

func throwsPanic(f func()) (b bool) {
	defer func() {
		if x := recover(); x != nil {
			b = true
		}
	}()
	f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
	return
}

主要数据结构

数组

切片

映射

每组文件被称为一个包。

Go 的包管理简单有清晰。

上面说的包寻找路劲是绝对路劲,还可以用相对路径:

import ./model //当前文件同一目录的model目录,但是不建议这种方式来import

还有集中导入的方式:

  1. 点操作
import(
   . "fmt"
)

这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println(“hello world”)可以省略的写成Println(“hello world”)

  1. 别名操作

别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字

import(
   f "fmt"
)

别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println(“hello world”)

  1. _操作

这个操作经常是让很多人费解的一个操作符,请看下面这个import

import (
    "database/sql"
    _ "github.com/ziutek/mymysql/godrv"
)

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。

类型系统

Go 就是 Go,从类型系统里,可以看到 C 的 struct、Python 的 self、Ruby 的 mixin等等,但都被优雅的融合了起来,有效且简洁。

发现没有,Go 没有类的概念,对,但它是用:

自定义类型

数据结构

type User struct {
  name string
  age int
}

这里是很像 C 语言的,但是 Go 里的类型还能有自己的方法,这个通过在定义函数时指明接收者来实现,而这样的函数就是面向对象里的方法。

func (u User) sayHi() {
  fmt.Println("My name is " + u.name)
}

func (u *User) changeName(name string) {
  u.name = name
}

func main()  {
  jack := User{name: "Jack", age: 28}

  jack.sayHi()
  jack.changeName("Lucy")
  jack.sayHi()
}

可以看到,指明接收者时,有值引用和指针引用。值引用传的是数据的副本,用于使用值;指针传递的是这个变量,用于修改。

定义接收者,这里有些像 Python 的类里,每个实例方法都有 self 的变量来指明当前实例就是接收者。

通过接受者的不同,可以实现多态。

package main

import (
	"fmt"
	"math"
)

type Rectangle struct {
	width, height float64
}

type Circle struct {
	radius float64
}

func (r Rectangle) area() float64 {
	return r.width*r.height
}

func (c Circle) area() float64 {
	return c.radius * c.radius * math.Pi
}


func main() {
	r1 := Rectangle{12, 2}
	r2 := Rectangle{9, 4}
	c1 := Circle{10}
	c2 := Circle{25}

	fmt.Println("Area of r1 is: ", r1.area())
	fmt.Println("Area of r2 is: ", r2.area())
	fmt.Println("Area of c1 is: ", c1.area())
	fmt.Println("Area of c2 is: ", c2.area())
}

嵌入类型

嵌入类型用于对现有类型的扩展和修改。

当一个 struct 嵌入到了另一个 struct 里后,那么这个被嵌入的 struct 所拥有的全部字段都被隐式地引入了当前定义的这个struct。字段重名的话,优先访问外面的,里面的通过 struct 名作为前缀访问。

同时,它的方法也会被间接继承过来。

type Admin struct {
  User
  level string
}

对,这就是 Go 的类型系统,简洁而有效。毕竟 C 语言只有 struct,配合着指针就无所不能。

函数作为值、类型

在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型

type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

函数作为类型好处是可以把这个类型的函数当做值来传递,这也是从 C 语言学的,请看下面的例子:

package main

import "fmt"

type testInt func(int) bool // 声明了一个函数类型

func isOdd(integer int) bool {
	if integer%2 == 0 {
		return false
	}
	return true
}

func isEven(integer int) bool {
	if integer%2 == 0 {
		return true
	}
	return false
}

// 声明的函数类型在这个地方当做了一个参数

func filter(slice []int, f testInt) []int {
	var result []int
	for _, value := range slice {
		if f(value) {
			result = append(result, value)
		}
	}
	return result
}

func main(){
	slice := []int {1, 2, 3, 4, 5, 7}
	fmt.Println("slice = ", slice)
	odd := filter(slice, isOdd)    // 函数当做值来传递了
	fmt.Println("Odd elements of slice are: ", odd)
	even := filter(slice, isEven)  // 函数当做值来传递了
	fmt.Println("Even elements of slice are: ", even)
}

函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。

interface

接口定义行为,在实现接口的类型里具体实现。

相比 Java 里的 interface,Go 不需要显式的声明实现了某个接口,主要它实现了接口里定义的方法,就自动默认是有了这个接口。 像 Ruby 里常说的 “Duck Type”。

既然不同的包或结构都实现了同一个接口,那么结合多态,就可以清晰勾画程序的框架了。

type notifier interface {
	notify()
}

type User struct {
  name string
  age int
}

func (u User) notify() {
  fmt.Println("HI, My name is " + u.name)
}

func main() {
  jack := User{name: "Jack", age: 28}
  sendNotification(jack)
}

func sendNotification(n notifier){
	n.notify()
}

空 interface

空 interface(interface{}) 不包含任何的 method,正因为如此,所有的类型都实现了空 interface。 空interface 对于描述起不到任何的作用(因为它不包含任何的 method ),但是空 interface 在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于 C 语言的 void* 类型。

// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s

一个函数把 interface{} 作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回 interface{},那么也就可以返回任意类型的值。

interface 变量存储的类型

interface 的变量里面可以存储任意类型的数值(该类型实现了 interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:

Go 语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T)。

这里 value 就是接口存的变量的值,ok是一个bool类型,element 是 interface 变量,T 是断言的类型。

如果 element 里面确实存储了T类型的数值,那么 ok 返回 true,否则返回 false。

if value, ok := element.(User); ok {
	//
}

而 switch 测试是解决多个类型判断的,有些像 Ruby 的 case,能判断类型:

for index, element := range list{
  switch value := element.(type) {
    case int:
      fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
    case string:
      fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
    case Person:
      fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
    default:
      fmt.Println("list[%d] is of a different type", index)
}

这里有一点需要强调的是: element.(type) 语法不能在switch外的任何逻辑里面使用。

嵌入 interface

如同 struct 的嵌入,能包含里面定义的方法。

源码包 container/heap 里面有这样的一个定义:

type Interface interface {
	sort.Interface //嵌入字段sort.Interface
	Push(x interface{}) //a Push method to push elements into the heap
	Pop() interface{} //a Pop elements that pops elements from the heap
}

sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:

type Interface interface {
	// Len is the number of elements in the collection.
	Len() int
	// Less returns whether the element with index i should sort
	// before the element with index j.
	Less(i, j int) bool
	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

反射

Go 实现了反射,所谓反射就是能检查程序在运行时的状态,一般用到的包是 reflect 包。 laws of reflection 里介绍了相关原理。

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

并发(Concurrency)

先说并行(Parallelism),它是让不同的代码片段同时在不同的物理处理器上运行。 而并发是指同时管理很多事情。Go 关注的主要是并发编程,“使用较少的资源做更多的事情”也是 Go 的设计哲学。

打个比方,四路汽车在四路马路上并行,但是当过仅有一个口的收费站时,就要管理协调着并发过。

Go 的并发主要是 goroutine 的并发,它是粒度更细的可执行单元。

goroutine,用 go 关键字来启动,这是它为什么叫 Go 语言的一个关键因素吧 —— 对并发的支持。

channel

goroutine 运行在相同的地址空间,因此访问共享内存必须做好同步,一般方法:

Go 的方法 —— channel:

channel 可以与 Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel 类型。 定义一个 channel 时,也需要定义发送到 channel 的值的类型。必须使用make 创建channel:

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

// channel通过操作符<-来接收和发送数据
ch <- v    // 发送 v 到 channel ch.
v := <-ch  // 从 ch 中接收数据,并赋值给v

一个例子:

package main

import "fmt"

func sum(a []int, c chan int) {
	total := 0
	for _, v := range a {
		total += v
	}
	c <- total  // send total to c
}

func main() {
	a := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(a[:len(a)/2], c)
	go sum(a[len(a)/2:], c)
	x, y := <-c, <-c  // receive from c

	fmt.Println(x, y, x + y)   // -5, 17, 12
}

上面例子中,需要读取两次c,这样不是很方便,Go 考虑到了这一点,所以也可以通过 range,像操作 slice 或者 map 一样操作缓存类型的 channel,请看下面的例子:

package main

import (
	"fmt"
	"time"
)

func fibonacci(n int, c chan int) {
	x, y := 1, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x + y
		time.Sleep(time.Second)
	}
	close(c)
}

func main() {
	c := make(chan int, 20)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

可以看到数据不断的输出来,这是因为 for i := range c 能够不断的读取channel里面的数据,直到该channel被显式的关闭。

上面代码我们看到可以显式的关闭 channel,生产者通过内置函数 close 关闭 channel。关闭 channel 之后就无法再发送任何数据了。

在消费方可以通过语法 v, ok := <-ch 测试 channel 是否被关闭。如果 ok 返回 false,那么说明 channel已经没有任何数据并且已经被关闭。

记住应该在生产者的地方关闭 channel,而不是消费的地方去关闭它,这样容易引起panic。 另外记住一点的就是 channel 不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束 range 循环之类的。

当有多个 channel 时,可以用 select 来选择使用准备好的那个(同时都准备好的话则随机选择):

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 1, 1
	for {
		select {
		case c <- x:
			x, y = y, x + y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

runtime包中有几个处理goroutine的函数:

并发模式

安装

Mint 下有官方最新的源:

sudo apt install golang-1.9

它会安装到 /usr/lib/go-1.9/ 目录下,之后需要 .bashrc 下设置环境:

# goenv
export GOROOT=/usr/lib/go/      # 这是一个连接,会指到当前的版本上,现在是 go-1.9/
export GOPATH=$HOME/go/         # 默认的用户自己的开发目录,也是 go get 下载下来的库
export PATH="$PATH:$GOROOT/bin/:$GOPATH/bin/"

感想

Go 项目能够编译成一个可执行文件,这是我最喜欢的。

Go 里是有指针的,这样就可以明确传递的是一个变量而不是其副本了。这是相比动态语言的清晰之处。

参考

本文由 付豪 创作,采用署名 4.0 国际(CC BY 4.0)创作共享协议进行许可,详细声明