GO.语言基础

Go程序设计的一些规则
Go之所以会那么简洁,是因为它有一些默认的行为:

大写字母开头的变量是可导出的,也就是其它包可以读取的,是公用变量;小写字母开头的就是不可导出的,是私有变量

大写字母开头的函数也是一样,相当于class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。

内建函数make、new
make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。

内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指

针,指向新分配的类型T的零值。

内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所

不同的原因是指向数据结构的引用在使用前必须被初始化。

例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始

化了内部的数据结构,填充适当的值。make返回初始化后的(非零)值。

函数

函数是Go里面的核心设计,它通过关键字func来声明,格式如:  ---支撑多个返回值,变参(不定数量的参数)

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    //这里是处理逻辑代码
    //返回多个值
    return value1, value2
}

Go语言中有种不错的设计,即延迟defer语句,你可以在函数中添加多个defer语句。

当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。

特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。

一般写打开一个资源有这样的操作:

func ReadWrite() bool {
	file.Open("file")
	// do something
	if failureX {
		file.Close()
		return false
	}
	if failureY {
		file.Close()
		return false
	}
	file.Close()
	return true
}

我们看到上面有很多重复的代码,Go的defer有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在defer后指定的函数会在函数退出前调用

如果有很多调用defer,那么defer是采用后进先出模式

func ReadWrite() bool {
	file.Open("file")
	defer file.Close()

	if failureX {
		return false
	}
	if failureY {
		return false
	}
	return true
}

函数作为值、类型

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

函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递

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)
}

函数当做值和类型在我们写一些通用接口的时候非常有用,觉得可以对比java等面向对象语言中的接口或者策略模式。

Panic和Recover

Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。

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

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

panic使用 :

var user = os.Getenv("USER")

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

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

func throwsPanic(f func()) (b bool) {
	defer func() {
		if x := recover(); x != nil {
			b = true
		}
	}()

	f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
	return
}

main函数 和 init函数

Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。

Go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。

程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包
中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。

import导入包文件

import(
     "fmt"
)

上面这个fmt是Go语言的标准库,其实是去goroot下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块:

1. 相对路径
import  “./model”   //当前文件同一目录的model目录,但是不建议这种方式来import

2. 绝对路径
import  “shorturl/model”   //加载gopath/src/shorturl/model模块

还有一些特殊的import方式:

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

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

3. _操作
import (
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)
_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。

struct类型

和其他语言一样,我们可以使用struct声明新的类型

type person struct {
	name string
	age  int
}

 Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。

package main

import "fmt"

type Human struct {
	name   string
	age    int
	weight int
}
type Student struct {
	speciality string
	Human      // 匿名字段,默认Student包含了Human所有字段
}

func main() {
	mark := Student{Human{"Mark", 25, 120}, "Computer Science"}

	fmt.Println("His speciality is ", mark.speciality)
	fmt.Println("His name is ", mark.name)

	mark.speciality = "AI"
	fmt.Println("His speciality is ", mark.speciality)

	mark.age = 46
	fmt.Println("His age is", mark.age)
}

这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?

Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过student.phone访问的时候,是访问student里面的字段,而不是human里面的字段。

这样就允许我们去通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。

面向对象

函数的另一种形态,带有接收者的函数,称为method。

package main

import "fmt"

type Rectangle struct {
	width, height float64
}

func area(r Rectangle) float64 {
	return r.width * r.height
}
func main() {
	r1 := Rectangle{12, 2}
	r2 := Rectangle{9, 4}
	fmt.Println("Area of r1 is: ", area(r1))
	fmt.Println("Area of r2 is: ", area(r2))
}

使用method的时候重要注意几点

1.虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样

2.method里面可以访问接收者的字段

3.调用method通过.访问,就像struct里面访问字段一样

值得说明的一点是,

Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。

另外,method可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。

例如,给自定义类型定义method

package main

import "fmt"

const (
	WHITE = iota
	BLACK
	BLUE
	RED
	YELLOW
)

type Color byte

type Box struct {
	width, height, depth float64
	color                Color
}

type BoxList []Box 

func (b Box) Volume() float64 { //调用者不会被修改
	return b.width * b.height * b.depth
}

func (b *Box) SetColor(c Color) { //调用者会被修改
	b.color = c
}

func (bl BoxList) BiggestsColor() Color {
	v := 0.00
	k := Color(WHITE)
	for _, b := range bl {
		if b.Volume() > v {
			v = b.Volume()
			k = b.color
		}
	}
	return k
}

func (bl BoxList) PaintItBlack() {
	for i, _ := range bl {
		bl[i].SetColor(BLACK)
	}
}
func (c Color) String() string {
	strings := []string{"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
	return strings[c]
}
func main() {
	boxes := BoxList{
		Box{4, 4, 4, RED},
		Box{10, 10, 1, YELLOW},
		Box{1, 1, 20, BLACK},
		Box{10, 10, 1, BLUE},
		Box{10, 30, 1, WHITE},
		Box{20, 20, 20, YELLOW},
	}
	fmt.Printf("We have %d boxes in our set
", len(boxes))
	fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
	fmt.Println("The color of the last one is", boxes[len(boxes)-1].color.String())
	fmt.Println("The biggest one is", boxes.BiggestsColor().String())
	fmt.Println("Let's paint them all black")
	boxes.PaintItBlack()
	fmt.Println("The color of the second one is", boxes[1].color.String())
	fmt.Println("Obviously, now, the biggest one is", boxes.BiggestsColor().String())
}

Color作为byte的别名,上面SetColor这个method,它的receiver是一个指向Box的指针(定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么

SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以我们需要传入指针。)

如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method,反之如果一个method的receiver是T,你也可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method

method继承与重写

类似于字段的继承,如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。

同样,与匿名字段冲突一样的道理,可以在包含匿名字段的struct上定义一个同样method,重写了匿名字段的方法

package main

import "fmt"

type Human struct {
	name  string
	age   int
	phone string
}
type Student struct {
	Human  //匿名字段
	school string
}
type Employee struct {
	Human   //匿名字段
	company string
}

func (h *Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s
", h.name, h.phone)
}

//Employee重写Human的method
func (e *Employee) SayHi() {
	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s
", e.name,
		e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
	mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
	sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
	mark.SayHi()
	sam.SayHi()
}

interface

简单的说,interface是一组method的组合,我们通过interface来定义对象的一组行为。如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。

如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。

package main

import "fmt"

type Human struct {
	name  string
	age   int
	phone string
}

type Student struct {
	school string
	loan   float32
	Human  //匿名字段
}

type Employee struct {
	company string
	money   float32
	Human   //匿名字段
}

//Human实现Sayhi方法
func (h Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s
", h.name, h.phone)
}

//Human实现Sing方法
func (h Human) Sing(lyrics string) {
	fmt.Println("La la la la...", lyrics)
}

//Employee重载Human的SayHi方法
func (e Employee) SayHi() {
	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s
", e.name,
		e.company, e.phone) //Yes you can split into 2 lines here.
}

// Interface Men被Human,Student和Employee实现
type Men interface {
	SayHi()
	Sing(lyrics string)
}

func main() {
	mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
	paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
	sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}

	var i Men

	i = mike
	i.SayHi()

	i = sam
	i.SayHi()

	x := make([]Men, 3)
	x[0], x[1], x[2] = paul, sam, mike
	for _, value := range x {
		value.SayHi()
	}
}

由上面的代码可以知道,interface可以被任意的对象实现。一个对象也可以实现任意多个interface,

其实,任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface,这点类似于java里的object。

如上面代码,如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。

interface函数参数

interface的变量可以持有任意实现该interface类型的对象,同样,也可以通过定义interface参数,让函数接受各种类型的参数。

interface变量存储的类型

如何反向知道interface变量中实际保存了的是哪个类型的对象。

Comma-ok断言

Go语言里面有一个语法: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

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

另一种语法 element.(type)只能在switch中使用,如果你要在switch外面判断一个类型就使用comma-ok。

package main

import (
	"fmt"
	"strconv"
)

type Element interface{}

type List []Element

type Person struct {
	name string
	age  int
}

//定义了String方法,实现了fmt.Stringer
func (p Person) String() string {
	return "(name: " + p.name + " - age: " + strconv.Itoa(p.age) + " years)"
}

func main() {
	list := make(List, 3)
	list[0] = 1
	list[1] = "Hello"
	list[2] = Person{"Dennis", 70}

	for index, element := range list {
		if value, ok := element.(int); ok {
			fmt.Printf("list[%d] is an int and its value is %d
", index, value)
		} else if value, ok := element.(string); ok {
			fmt.Printf("list[%d] is a string and its value is %s
", index, value)
		} else if value, ok := element.(Person); ok {
			fmt.Printf("list[%d] is a Person and its value is %s
", index, value)
		} else {
			fmt.Println("list[%d] is of a different type", index)
		}
	}
}

 嵌入interface

如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。

源码包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
}

 另一个例子就是io包下面的 io.ReadWriter ,他包含了io包下面的Reader和Writer两个interface。

// io.ReadWriter
type ReadWriter interface {
	Reader
	Writer
}

 反射

Go语言实现了反射,所谓反射就是动态运行时的状态。我们一般用到的包是reflect包。使用reflect一般分成三步:

1.要去反射是一个类型的值(都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数):

t := reflect.TypeOf(i) //得到类型的元数据,通过 t 我们能获取类型定义里面的所有元素

v := reflect.ValueOf(i) //得到实际的值,通过 v 我们获取存储在里面的值,还可以去改变值

2.转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值:

tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签

name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值

获取反射值能返回相应的类型和数值

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())

最后,反射的话,那么反射的字段必须是可修改的,前面说过传值和传引用的区别,反射的字段必须是可读写的意思是,

如果下面这样写,那么会发生错误

var x float64 = 3.4

v := reflect.ValueOf(x)

v.SetFloat(7.1)

如果要修改相应的值,必须这样写

var x float64 = 3.4

p := reflect.ValueOf(&x)

v := p.Elem()

v.SetFloat(7.1)

GO并发

GO从语言层面就支持了并行

goroutine

goroutine是Go并行设计的核心。goroutine说到底其实就是线程,但是他比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine

之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易

用、更高效、更轻便。goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过go关键字实现了,其实就是一个普通的函数:go hello(a, b, c)

channels

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) {
	sum := 0
	for _, v := range a {
		sum += v
	}
	c <- sum // send sum to c
}
func main() {
	a := []int{7, 2, 8, -9, 4, 0}

	fmt.Println(len(a))
	fmt.Println(a[:3])

	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)
}

 默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。

Buffered Channels

默认的是非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。

ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞。

package main

import (
	"fmt"
)

func main() {
	c := make(chan int, 2) //修改2为1就报错,修改2为3可以正常运行
	c <- 1
	c <- 2
	fmt.Println(<-c)
	fmt.Println(<-c)
}

Range和Close

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

package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 1, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}
func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

Select

如果多个channel,Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。

select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个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)
}

c和quit,随机选择一个准备好的执行,两个线程执行,func线程中,先将c打印完在给quit赋值。

select其实就是类似switch的功能,在select里面还有default语法,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。

超时

有时候会出现goroutine阻塞的情况,可以利用select来设置超时,来避免整个的程序进入阻塞。

package main

import "time"

func main() {
	c := make(chan int)
	o := make(chan bool)
	go func() {
		for {
			select {
			case v := <-c:
				println(v)
			case <-time.After(5 * time.Second):
				println("timeout")
				o <- true
				break
			}
		}
	}()
	println(<-o)
}

runtime goroutine

runtime包中有几个处理goroutine的函数:
Goexit:退出当前执行的goroutine,但是defer函数还会继续调用
Gosched:让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
NumCPU:返回 CPU 核数量
NumGoroutine:返回正在执行和排队的任务总数
GOMAXPROCS:用来设置可以运行的CPU核数

#笔记内容来自 《Go Web编程》

原文地址:https://www.cnblogs.com/shanhm1991/p/7144356.html