GO系列—基础知识指南

Go语言基础知识点html

1. 说明

2. 环境安装

2.1. 配置的三个环境变量说明

  • go env命令能够查看Go语言的环境变量设置
  • GOROOT:这个是Go语言的安装目录
  • GOPATH:这个是Go语言的工做目录
    • GOPATH能够有多个用符号分割,(多个目录的时候Windows是分号,Linux系统是冒号)。可是第下载的包会放在第一个下面
    • 从go 1.8开始,GOPATH环境变量如今有一个默认值,若是它没有被设置。 它在Unix上默认为$HOME/go,在Windows上默认为%USERPROFILE%/go。
  • GOBIN:这个实际上指向的是%GOPATH%/bin目录

2.2. linux

  • 首先,根据对应的操做系统选择安装包下载,在这里我使用的是Centos 64位系统

wget https://studygolang.com/dl/golang/go1.9.2.linux-amd64.tar.gzmysql

sudo tar -xzf go1.8.3.linux-amd64.tar.gz -C /usr/locallinux

  • 配置 /etc/profile

vi /etc/profilegit

  • 添加环境变量GOROOT和将GOBIN添加到PATH中

export GOROOT=/usr/local/gogithub

export PATH=$PATH:$GOROOT/bingolang

  • 添加环境变量GOPATH(这个可按实际状况设置目录位置)

export GOPATH=/usr/local/go/pathweb

  • 配置完毕后,执行命令令其生效

source /etc/profilesql

  • 在控制台输入go version,若输出版本号则安装成功

2.3. windows

2.4. 目录结构介绍

首先,咱们在解压的时候会获得一个名为go的文件夹,其中包括了全部Go语言相关的一些文件,在这下面又包含不少文件夹和文件,咱们来简单说明其中主要文件夹的做为:mongodb

  • api:用于存放依照Go版本顺序的API增量列表文件。这里所说的API包含公开的变量、常量、函数等。这些API增量列表文件用于Go语言API检查
  • bin:用于存放主要的标准命令文件(可执行文件),包含go、godoc、gofmt
  • blog:用于存放官方博客中的全部文章
  • doc:用于存放标准库的HTML格式的程序文档。咱们能够经过godoc命令启动一个Web程序展现这些文档
  • lib:用于存放一些特殊的库文件
  • misc:用于存放一些辅助类的说明和工具
  • pkg:用于存放安装Go标准库后的全部归档文件(以.a结尾的文件)。注意,你会发现其中有名称为linux_amd64的文件夹,咱们称为平台相关目录。这类文件夹的名称由对应的操做系统和计算架构的名称组合而成。经过go install命令,Go程序会被编译成平台相关的归档文件存放到其中
  • src:用于存放Go自身、Go标准工具以及标准库的全部源码文件
  • test:存放用来测试喝验证Go自己的全部相关文件

2.5. 其余概念介绍

2.5.1. 工做区

这在Go中是一个很是重要的概念,在通常状况下,Go源码文件必须放在工做区中,也就是说,咱们写的项目代码都必须放在咱们所设定的工做区中,虽然对于命令源码文件来讲,这不是必须的。但咱们大多都是前一种状况。工做区其实就是一个对应特定工程的目录,它应包含3个子目录:shell

  • src目录:存放源代码(好比:.go .c .h .s等)
  • pkg目录:编译后生成的文件(好比:.a)
  • bin目录:编译后生成的可执行文件(为了方便,能够把此目录加入到 $PATH 变量中,若是有多个gopath,那么使用${GOPATH//://bin:}/bin添加全部的bin目录)

2.5.2. 命令源文件

若是一个源码文件被声明属于main代码包,且该文件代码中包含无参数声明喝结果声明的main函数,则它就是命令源码文件。命令源码文件可经过go run命令直接启动运行

2.5.3. .a文件

2.5.3.1. 编译应用

  • 上面咱们已经创建了本身的应用包,如何进行编译安装呢?有两种方式能够进行安装
  • 一、只要进入对应的应用包目录,而后执行go install,就能够安装了
  • 二、在任意的目录执行以下代码go install mymath
安装完以后,咱们能够进入以下目录

cd $GOPATH/pkg/${GOOS}_${GOARCH}
//能够看到以下文件
mymath.a

这个.a文件是应用包,那么咱们如何进行调用呢?

接下来咱们新建一个应用程序来调用这个应用包

新建应用包mathapp

cd $GOPATH/src
mkdir mathapp
cd mathapp
vim main.go

$GOPATH/src/mathapp/main.go源码:

package main

import (
    "mymath"
    "fmt"
)

func main() {
    fmt.Printf("Hello, world.  Sqrt(2) = %v\n", mymath.Sqrt(2))
}

能够看到这个的package是main,import里面调用的包是mymath,这个就是相对于$GOPATH/src的路径,若是是多级目录,就在import里面引入多级目录,若是你有多个GOPATH,也是同样,Go会自动在多个$GOPATH/src中寻找。

如何编译程序呢?进入该应用目录,而后执行go build,那么在该目录下面会生成一个mathapp的可执行文件

./mathapp

输出以下内容

Hello, world.  Sqrt(2) = 1.414213562373095

如何安装该应用,进入该目录执行go install,那么在$GOPATH/bin/下增长了一个可执行文件mathapp, 还记得前面咱们把$GOPATH/bin加到咱们的PATH里面了,这样能够在命令行输入以下命令就能够执行

mathapp

也是输出以下内容

Hello, world.  Sqrt(2) = 1.414213562373095

这里咱们展现如何编译和安装一个可运行的应用,以及如何设计咱们的目录结构。

2.5.4. bin和pkg目录干什么用的 ??????

  • 目前说不清楚

2.5.5. go build和go install还有go run,go get的区别

  • go run:go run 编译并直接运行程序,它会产生一个临时文件(但不会生成 .exe 文件),直接在命令行输出程序执行结果,方便用户调试。
  • go build:go build 用于测试编译包,主要检查是否会有编译错误,若是是一个可执行文件的源码(便是 main 包),就会直接生成一个可执行文件到当前的命名执行目录。
  • go install:go install 的做用有两步:第一步是编译导入的包文件,全部导入的包文件编译完才会编译主程序;第二步是将编译后生成的可执行文件放到 bin 目录下($GOPATH/bin),编译后的包文件放到 pkg 目录下($GOPATH/pkg)。
  • go get:go get会作两件事:
    • 从远程下载须要用到的包
    • 执行go install
  • 下面的连接是各命令的执行结果:https://blog.csdn.net/zyz770834013/article/details/78656985

2.5.6. GO命令介绍

3. 关键字

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


var和const Go语言基础里面的变量和常量申明
package和import已经有太短暂的接触
func 用于定义函数和方法
return 用于从函数返回
defer 用于相似析构函数
go 用于并发
select 用于选择不一样类型的通信
interface 用于定义接口
struct 用于定义抽象数据类型
break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
chan用于channel通信
type用于声明自定义类型
map用于声明map类型数据
range用于读取slice、map、channel数据

4. 包

package是最基本的分发单位和工程管理中依赖关系的体现

4.1. 包概念

  • 多个文件可能被打包在一块儿,由于包名是同样的,有些文件里面的变量没有定义,可是可能在其余文件中定义了,只是包名同样,打包到一块儿了
  • 而后咱们在go中是按照文件夹引用的
  • Foo 和 FOO 都是被导出的名称。名称 foo 是不会被导出的。大写的函数名是被导出的
  • 每一个Go语言源代码文件开头都必需要有一个package声明,表示源代码文件所属包
  • 要生成Go语言可执行程序,必需要有名为main的package包,且在该包下必须有且只有一个main函数
  • 同一个路径下只能存在一个package,一个package能够由多个源代码文件组成

4.2. 包引用原理

  • 跟package相似,import原理遵照如下几个原则:
  • 若是一个main导入其余的包,包会被顺序导入
  • 若是导入的包(pkg1)依赖其余的包(包pkg2),会首先导入pkg2,而后初始化pkg2中的常量与变量,若是pkg2中有init函数,会自动执行init
  • 全部包导入完成后才会对main的常量和变量进行初始化,而后执行main中的init函数(若是有的话),最后执行main函数
  • 若是一个包被导入屡次实际上只会导入一次

4.3. import

4.3.1. 普通引入

  • 相对路径:import “./model” //当前文件同一目录的model目录,可是不建议这种方式来import
  • 绝对路径:import “shorturl/model” //加载gopath/src/shorturl/model模块
import "fmt"

    inport (
        "fmt"
    )

    import (
        "log"
        "fmt"

        "github.com/jinzhu/gorm"
        _ "github.com/jinzhu/gorm/dialects/mysql"

        "gin-blog/pkg/setting"
    )

4.3.2. 特殊用法

  • 一、.操做:这个点操做的含义就是这个包导入以后在你调用这个包的函数时,你能够省略前缀的包名,也就是前面你调用的fmt.Println("hello world")能够省略的写成Println("hello world")
import(
     . "fmt"
 )
  • 二、别名操做:别名操做顾名思义咱们能够把包命名成另外一个咱们用起来容易记忆的名字
import(
     f "fmt"
 )
  • 三、操做:操做实际上是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数
import (
        "database/sql"
        _ "github.com/ziutek/mymysql/godrv"
    )

5. 变量

5.1. 变量定义

  • 函数外的每一个语句都必须以关键字开始(varfunc、等等)
  • 在函数中,:= 简洁赋值语句在明确类型的地方,能够用于替代 var 定义。
  • := 这种形式有个局限只能用在函数内部,因此通常用var方式来定义全局变量
  • _(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。
  • 对于没有给类型的变量,Go会根据其相应值的类型推倒来帮你初始化它们
  • Go对于已声明但未使用的变量会在编译阶段报错,好比下面的代码就会产生一个错误:声明了i但未使用。
package main

func main() {
    var i int
}

5.2. 变量赋值

var variableName type
var vname1, vname2, vname3 type
var variableName type = value
var vname1, vname2, vname3 type= v1, v2, v3
// Go会根据其相应值的类型来帮你初始化它们
var vname1, vname2, vname3 = v1, v2, v3
vname1, vname2, vname3 := v1, v2, v3

5.3. 分组声明

  • 在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。
import "fmt"
import "os"

const i = 100
const pi = 3.1415
const prefix = "Go_"

var i int
var pi float32
var prefix string

能够分组写成以下形式:

import(
    "fmt"
    "os"
)

const(
    i = 100
    pi = 3.1415
    prefix = "Go_"
)

var(
    i int
    pi float32
    prefix string
)

5.4. 常量

  • 所谓常量,也就是在程序编译阶段就肯定下来的值,而程序在运行时没法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。
  • Go常量和通常程序语言不一样的是,能够指定至关多的小数位数(例如200位), 若指定給float32自动缩短为32bit,指定给float64自动缩短为64bit
  • 常量不能使用 := 语法定义。
  • 常量一般不须要指定类型,若是须要,也能够明确指定常量的类型
const constantName = value
const Pi float32 = 3.1415926

5.5. 零值

  • 变量在定义时没有明确的初始化时会赋值为_零值_。
  • 关于“零值”,所指并不是是空值,而是一种“变量未填充前”的默认值,一般为0。
  • 零值是
int     0
int8    0
int32   0
int64   0
uint    0x0
rune    0 //rune的实际类型是 int32
byte    0x0 // byte的实际类型是 uint8
float32 0 //长度为 4 byte
float64 0 //长度为 8 byte
bool    false
string  ""

6. 内置基础类型

6.1. 基础类型

  • bool
  • string

    • 字符串是用一对双引号("")或反引号(`)括起来定义
    • 在Go中字符串是不可变的,例以下面的代码编译时会报错:cannot assign to s[0]
    var s string = "hello"
      s[0] = 'c'
    • 但若是真的想要修改怎么办呢?下面的代码能够实现
    s := "hello"
      c := []byte(s)  // 将字符串 s 转换为 []byte 类型
      c[0] = 'c'
      s2 := string(c)  // 再转换回 string 类型
      fmt.Printf("%s\n", s2)
    • Go中可使用+操做符来链接两个字符串
    s := "hello,"
      m := " world"
      a := s + m
      fmt.Printf("%s\n", a)
    • 修改字符串也可写为
    s := "hello"
      s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操做
      fmt.Printf("%s\n", s)
    • 声明多行字符串,能够经过`来声明, 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出
    m := `hello
                    world`
  • 整数类型

    • 无符号:int int8 int16 int32 int64
    • 带符号:uint uint8 uint16 uint32 uint64 uintptr
    • 其中rune是int32的别称,byte是uint8的别称
    • 这些类型的变量之间不容许互相赋值或操做,否则会在编译时引发编译器报错。
  • 浮点型
    • float32 float64
    • (没有float类型),默认是float64
  • 复数
    • complex64 complex128
    • 它的默认类型是complex128(64位实数+64位虚数)。若是须要小一些的,也有complex64(32位实数+32位虚数)。复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位
  • 错误类型

    • Go内置有一个error类型,专门用来处理错误信息,Go的package里面还专门有一个包errors来处理错误
    err := errors.New("emit macho dwarf: elf header corrupted")
      if err != nil {
          fmt.Print(err)
      }

6.2. 类型转换

表达式 T(v) 将值 v 转换为类型 T

一些关于数值的转换:

var i int = 42 var f float64 = float64(i) var u uint = uint(f)

或者,更加简单的形式:

i := 42 f := float64(i) u := uint(f)

与 C 不一样的是 Go 的在不一样类型之间的项目赋值时须要显式转换。 试着移除例子中 float64 或 int 的转换看看会发生什么。

6.3. 类型推倒

在定义一个变量但不指定其类型时(使用没有类型的 var 或 := 语句), 变量的类型由右值推导得出。

当右值定义了类型时,新变量的类型与其相同:

var i int j := i // j 也是一个 int

可是当右边包含了未指名类型的数字常量时,新的变量就多是 int 、 float64 或 complex128。 这取决于常量的精度:

i := 42 // int f := 3.142 // float64 g := 0.867 + 0.5i // complex128

尝试修改演示代码中 v 的初始值,并观察这是如何影响其类型的。

7. 复杂数据类型

7.1. iota枚举

<http://www.cnblogs.com/zsy/p/5370052.html>
Go里面有一个关键字iota,这个关键字用来声明enum的时候采用,它默认开始值是0,const中每增长一行加1:

package main

import (
    "fmt"
)

const (
    x = iota // x == 0
    y = iota // y == 1
    z = iota // z == 2
    w        // 常量声明省略值时,默认和以前一个值的字面相同。这里隐式地说w = iota,所以w == 3。其实上面y和z可一样不用"= iota"
)

const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0

const (
    h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
)

const (
    a       = iota //a=0
    b       = "B"
    c       = iota             //c=2
    d, e, f = iota, iota, iota //d=3,e=3,f=3
    g       = iota             //g = 4
)

func main() {
    fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v)
}

    除非被显式设置为其它值或iota,每一个const分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,若是前面那个常量的值是iota,则它也被设置为iota。

7.2. 指针

Go 具备指针。 指针保存了变量的内存地址。

类型 *T 是指向类型 T 的值的指针。其零值是 nil

var p *int int型的指针

& 符号会生成一个指向其做用对象的指针。

i := 42 p = &i

  • 符号表示指针指向的底层的值。

fmt.Println(p) // 经过指针 p 读取 i p = 21 // 经过指针 p 设置 i

这也就是一般所说的“间接引用”或“非直接引用”。

与 C 不一样,Go 没有指针运算。

package main

import "fmt"

func main() { i, j := 42, 2701

p := &i         // point to i
fmt.Println(*p) // read i through the pointer
*p = 21         // set i through the pointer
fmt.Println(i)  // see the new value of i

p = &j         // point to j
*p = *p / 37   // divide j through the pointer
fmt.Println(j) // see the new value of j

}

7.3. 结构体

Go语言中,也和C或者其余语言同样,咱们能够声明新的类型,做为其它类型的属性或字段的容器。例如,咱们能够建立一个自定义类型person表明一我的的实体。这个实体拥有属性:姓名和年龄。这样的类型咱们称之struct

7.3.1. 声明

  • 普通
type person struct {
    name string
    age int
}

var P person  // P如今就是person类型的变量了

P.name = "Astaxie"  // 赋值"Astaxie"给P的name属性.
P.age = 25  // 赋值"25"给变量P的age属性
fmt.Printf("The person's name is %s", P.name)  // 访问P的name属性.
  • 按照顺序提供初始化值:P := person{"Tom", 25}
  • 经过field:value的方式初始化,这样能够任意顺序:P := person{age:24, name:"Tom"}
  • 固然也能够经过new函数分配一个指针,此处P的类型为*person:P := new(person)

7.3.2. struct的匿名字段(嵌入字段)

  • 当匿名字段是一个struct的时候,那么这个struct所拥有的所有字段都被隐式地引入了当前定义的这个struct
package main

import "fmt"

type Human struct {
    name string
    age int
    weight int
}

type Student struct {
    Human  // 匿名字段,那么默认Student就包含了Human的全部字段
    speciality string
}

func main() {
    // 咱们初始化一个学生
    mark := Student{Human{"Mark", 25, 120}, "Computer Science"}

    // 咱们访问相应的字段
    fmt.Println("His name is ", mark.name)
    fmt.Println("His age is ", mark.age)
    fmt.Println("His weight is ", mark.weight)
    fmt.Println("His speciality is ", mark.speciality)
    // 修改对应的备注信息
    mark.speciality = "AI"
    fmt.Println("Mark changed his speciality")
    fmt.Println("His speciality is ", mark.speciality)
    // 修改他的年龄信息
    fmt.Println("Mark become old")
    mark.age = 46
    fmt.Println("His age is", mark.age)
    // 修改他的体重信息
    fmt.Println("Mark is not an athlet anymore")
    mark.weight += 60
    fmt.Println("His weight is", mark.weight)
}
  • 嵌入字段也能够做为字段名去使用
mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1
  • 经过匿名访问和修改字段至关的有用,可是不只仅是struct字段哦,全部的内置类型和自定义类型都是能够做为匿名字段的
package main

import "fmt"

type Skills []string

type Human struct {
    name string
    age int
    weight int
}

type Student struct {
    Human  // 匿名字段,struct
    Skills // 匿名字段,自定义的类型string slice
    int    // 内置类型做为匿名字段
    speciality string
}

func main() {
    // 初始化学生Jane
    jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
    // 如今咱们来访问相应的字段
    fmt.Println("Her name is ", jane.name)
    fmt.Println("Her age is ", jane.age)
    fmt.Println("Her weight is ", jane.weight)
    fmt.Println("Her speciality is ", jane.speciality)
    // 咱们来修改他的skill技能字段
    jane.Skills = []string{"anatomy"}
    fmt.Println("Her skills are ", jane.Skills)
    fmt.Println("She acquired two new ones ")
    jane.Skills = append(jane.Skills, "physics", "golang")
    fmt.Println("Her skills now are ", jane.Skills)
    // 修改匿名内置类型字段
    jane.int = 3
    fmt.Println("Her preferred number is", jane.int)
}
  • 嵌入字段属性名和原始属性名重名处理规则是:最外层的优先访问,固然若是咱们想访问重载后对应匿名类型里面的字段,能够经过匿名字段名来访问
package main

import "fmt"

type Human struct {
    name string
    age int
    phone string  // Human类型拥有的字段
}

type Employee struct {
    Human  // 匿名字段Human
    speciality string
    phone string  // 雇员的phone字段
}

func main() {
    Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
    fmt.Println("Bob's work phone is:", Bob.phone)
    // 若是咱们要访问Human的phone字段
    fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}

7.3.3. 结构体指针????????

  • 经过指针间接的访问是透明的,其实我不是很明白这句话是作什么的
package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    p := &v
    p.X = 1e9
    fmt.Println(v)
}
 结构体字段能够经过结构体指针来访问。

7.3.4. StructTag

7.3.4.1. 来源

  • 要先了解一下golang的基础,在golang中,命名都是推荐都是用驼峰方式,而且在首字母大小写有特殊的语法含义:包外没法引用。可是由常常须要和其它的系统进行数据交互,例如转成json格式,存储到mongodb啊等等。这个时候若是用属性名来做为键值可能不必定会符合项目要求。
  • 因此呢就多了小米点的内容,在golang中叫标签(Tag),在转换成其它数据格式的时候,会使用其中特定的字段做为键值
u := &User{UserId: 1, UserName: "tony"}
j, _ := json.Marshal(u)
fmt.Println(string(j))
// 输出内容:{"user_id":1,"user_name":"tony"}

 若是在属性中不增长标签说明,则输出:

{"UserId":1,"UserName":"tony"}

能够看到直接用struct的属性名作键值。

其中还有一个bson的声明,这个是用在将数据存储到mongodb使用的。

7.3.4.2. 语法

  • "号的貌似是注释
  • `号的貌似是转换的别名,和一些第三方的特性,经过反射实现

7.3.4.3. 取值

t := reflect.TypeOf(u)
field := t.Elem().Field(0)
fmt.Println(field.Tag.Get("json"))
fmt.Println(field.Tag.Get("bson"))
1 package main
 2 import (
 3     "fmt"
 4     "reflect" // 这里引入reflect模块
 5 )
 6 type User struct {
 7     Name   string "user name" //这引号里面的就是tag
 8     Passwd string "user passsword"
 9 }
10 func main() {
11     user := &User{"chronos", "pass"}
12     s := reflect.TypeOf(user).Elem() //经过反射获取type定义
13     for i := 0; i < s.NumField(); i++ {
14         fmt.Println(s.Field(i).Tag) //将tag输出出来
15     }
16 }
1 package main
 2  
 3 import (
 4     "fmt"
 5     "reflect"
 6 )
 7  
 8 func main() {
 9     type S struct {
10         F string `species:"gopher" color:"blue"`
11     }
12  
13     s := S{}
14     st := reflect.TypeOf(s)
15     field := st.Field(0)
16     fmt.Println(field.Tag.Get("color"), field.Tag.Get("species"))
17  
18 }

7.4. 数组

  • var arr [n]type:在[n]type中,n表示数组的长度,type表示存储元素的类型
package main
    import "fmt"
    func main() {
        var a [2]string
        a[0] = "Hello"
        a[1] = "World"
        fmt.Println(a[0], a[1])
        fmt.Println(a)
    }
  • 因为长度也是数组类型的一部分,所以[3]int与[4]int是不一样的类型
  • 数组的长度不能改变
  • 数组之间的赋值是值的赋值,即当把一个数组做为参数传入函数的时候,传入的实际上是该数组的副本,而不是它的指针,若是要使用指针,那么就须要用到后面介绍的slice类型了
  • 数组可使用另外一种:=来声明
a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组
    b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为一、二、3,其它默认为0
    c := [...]int{4, 5, 6} // 能够省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度☆☆☆☆☆
  • Go支持嵌套数组,即多维数组
// 声明了一个二维数组,该数组以两个数组做为元素,其中每一个数组中又有4个int类型的元素
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}

// 上面的声明能够简化,直接忽略内部的类型
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}

7.5. 切片

7.5.1. 定义

  • 在不少应用场景中,数组并不能知足咱们的需求。在初始定义数组时,咱们并不知道须要多大的数组,所以咱们就须要“动态数组”。在Go里面这种数据结构叫slice
  • slice的声明也能够像array同样,只是不须要长度。
package main

    import "fmt"

    func main() {
        p := []int{2, 3, 5, 7, 11, 13}
        fmt.Println("p ==", p)

        for i := 0; i < len(p); i++ {
            fmt.Printf("p[%d] == %d\n", i, p[i])
        }
    }

7.5.2. 对数组和silce切片

  • slice能够从一个数组或一个已经存在的slice中再次声明。slice经过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i。
  • slice的默认开始位置是0,ar[:n]等价于ar[0:n]
  • slice的第二个序列默认是数组的长度,ar[n:]等价于ar[n:len(ar)]
  • 若是从一个数组里面直接获取slice,能够这样ar[:],由于默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]
// 声明一个数组
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个slice
var aSlice, bSlice []byte

// 演示一些简便操做
aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c
aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
aSlice = array[:]  // 等价于aSlice = array[0:10] 这样aSlice包含了所有的元素

// 从slice中获取slice
aSlice = array[3:7]  // aSlice包含元素: d,e,f,g,len=4,cap=7
bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
bSlice = aSlice[:3]  // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
bSlice = aSlice[0:5] // 对slice的slice能够在cap范围内扩展,此时bSlice包含:d,e,f,g,h
bSlice = aSlice[:]   // bSlice包含全部aSlice的元素: d,e,f,g

7.5.3. nil slice

  • slice 的零值是 nil
  • 一个 nil 的 slice 的长度和容量是 0。
package main

    import "fmt"

    func main() {
        var z []int
        fmt.Println(z, len(z), cap(z))
        if z == nil {
            fmt.Println("nil!")
        }
    }

7.5.4. slice是引用类型

  • slice并非真正意义上的动态数组,而是一个引用类型。slice老是指向一个底层array
  • slice是引用类型,因此当引用改变其中元素的值时,其它的全部引用都会改变该值,例如上面的aSlice和bSlice,若是修改了aSlice中元素的值,那么bSlice相对应的值也会改变。
  • slice像一个结构体,这个结构体包含了三个元素
    • 一个指针,指向数组中slice指定的开始位置
    • 长度,即slice的长度
    • 最大长度(容量),也就是slice开始位置到数组的最后位置的长度
  • slice内置的append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其它slice。 但当slice中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的slice则不受影响。

7.5.5. 构造slice ????????

slice 由函数 make 建立。这会分配一个零长度的数组而且返回一个 slice 指向这个数组:

a := make([]int, 5) // len(a)=5

为了指定容量,可传递第三个参数到 make

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5 b = b[1:] // len(b)=4, cap(b)=4

package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a)
    b := make([]int, 0, 5)
    printSlice("b", b)
    c := b[:2]
    printSlice("c", c)
    d := c[2:5]
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}

7.5.6. 三个参数????????

从Go1.2开始slice支持了三个参数的slice,以前咱们一直采用这种方式在slice或者array基础上来获取一个slice

var array [10]int slice := array[2:4]

这个例子里面slice的容量是8,新版本里面能够指定这个容量

slice = array[2:4:7]

上面这个的容量就是7-2,即5。这样这个产生的新的slice就没办法访问最后的三个元素。

若是slice是这样的形式array[:i:j],即第一个参数为空,默认值就是0。

7.5.7. 向slice添加元素

  • 对于slice有几个有用的内置函数:

    • len 获取slice的长度
    • cap 获取slice的最大容量
    • copy 函数copy从源slice的src中复制元素到目标dst,而且返回复制的元素的个数
    • append 向slice里面追加一个或者多个元素,而后返回一个和slice同样类型的slice

      • append 的第一个参数 s 是一个类型为 T 的数组,其他类型为 T 的值将会添加到 slice。
      • 若是 s 的底层数组过小,而不能容纳全部值时,会分配一个更大的数组。 返回的 slice 会指向这个新分配的数组。

        package main
        
          import "fmt"
        
          func main() {
              var a []int
              printSlice("a", a)
        
              // append works on nil slices.
              a = append(a, 0)
              printSlice("a", a)
        
              // the slice grows as needed.
              a = append(a, 1)
              printSlice("a", a)
        
              // we can add more than one element at a time.
              a = append(a, 2, 3, 4)
              printSlice("a", a)
          }
        
          func printSlice(s string, x []int) {
              fmt.Printf("%s len=%d cap=%d %v\n",
                  s, len(x), cap(x), x)
          }

7.5.8. 切片的扩容原理

  • append增长容量是按照若是容量不够把以前切片的容量乘以2,若是乘以2还不够就以前容量+1乘以2来递增的

7.6. range

for 循环的 range 格式能够对 slice 或者 map 进行迭代循环。

package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d = %d\n", i, v)
    }
}

能够经过赋值给 _ 来忽略序号和值。 若是只须要索引值,去掉“, value”的部分便可。

package main

import "fmt"

func main() {
    pow := make([]int, 10)
    for i := range pow {
        pow[i] = 1 << uint(i)
    }
    for _, value := range pow {
        fmt.Printf("%d\n", value)
    }
}

7.7. map

7.7.1. 定义

  • map在使用以前必须用 make 而不是 new 来make初始化
  • 空map的值为 nil,而且不能赋值
package main

    import "fmt"

    type Vertex struct {
        Lat, Long float64
    }

    var m map[string]Vertex

    func main() {
        m = make(map[string]Vertex)
        m["Bell Labs"] = Vertex{
            40.68433, -74.39967,
        }
        fmt.Println(m["Bell Labs"])
    }
  • map是无序的,每次打印出来的map都会不同,它不能经过index获取,而必须经过key获取
  • map的长度是不固定的,也就是和slice同样,也是一种引用类型
  • map和其余基本型别不一样,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制???

7.7.2. 修改map

// 一、初始化一个字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }

// 二、插入元素
m[key] = elem

// 三、得到元素
elem = m[key]

// 四、map有两个返回值,第二个返回值,若是存在ok为true,若是不存在key,那么ok为false,而且 elem 是 map 的元素类型的零值
csharpRating, ok := rating["C#"]
if ok {
    fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
    fmt.Println("We have no rating associated with C# in the map")
}

// 五、删除元素
delete(rating, "C")  // 删除key为C的元素

7.7.3. make和new操做

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

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

    new返回指针。

  • 内建函数make(T, args)与new(T)有着不一样的功能,make只能建立slice、map和channel,而且返回一个有初始值(非零)的T类型,而不是*T。本质来说,致使这三个类型有所不一样的缘由是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化以前,slice为nil。对于slice、map和channel来讲,make初始化了内部的数据结构,填充适当的值。

    make返回初始化后的(非零)值。

  • 总结
    • make用于内建类型(只能用于建立map、slice 和channel)的内存分配。而且返回一个有初始值(非零)的T类型,而不是*T。
    • new用于各类类型的内存分配。new(T)分配了零值填充的T类型的内存空间,而且返回其地址,即一个T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点很是重要:*new返回指针。

8. 流程控制

8.1. for循环

Go里面最强大的一个控制逻辑就是for,它既能够用来循环读取数据,又能够看成while来控制逻辑,还能迭代操做

8.1.1. 标准写法

package main

import "fmt"

func main(){
    sum := 0;
    for index:=0; index < 10 ; index++ {
        sum += index
    }
    fmt.Println("sum is equal to ", sum)
}
// 输出:sum is equal to 45

8.1.2. 省略

  • 咱们能够忽略前置、后置语句为空。
sum := 1
for ; sum < 1000;  {
    sum += sum
}
  • 其中;也能够省略,那么就变成以下的代码了,是否是似曾相识?对,这就是while的功能
package main

import "fmt"

func main() {
    sum := 1
    for sum < 1000 {
        sum += sum
    }
    fmt.Println(sum)
}

8.1.3. 结合range

  • for配合range能够用于读取slice和map的数据
for k,v:=range map {
    fmt.Println("map's key:",k)
    fmt.Println("map's val:",v)
}
  • 因为 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种状况下, 可使用_来丢弃不须要的返回值
for _, v := range map{
    fmt.Println("map's val:", v)
}

8.1.4. 死循环

  • 若是省略了循环条件,循环就不会结束,所以能够用更简洁地形式表达死循环。
package main

func main() {
    for {
    }
}

8.2. if语句

  • if有一个强大的地方就是条件判断语句里面容许声明一个变量,这个变量的做用域只能在该条件逻辑块内,其余地方就不起做用了
if x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}
// 计算获取值x,而后根据x返回的大小,判断是否大于10。
if x := computedValue(); x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}

//这个地方若是这样调用就编译出错了,由于x是条件里面的变量
fmt.Println(x)
if integer == 3 {
    fmt.Println("The integer is equal to 3")
} else if integer < 3 {
    fmt.Println("The integer is less than 3")
} else {
    fmt.Println("The integer is greater than 3")
}

8.3. switch

8.3.1. 标准形式

switch sExpr {
case expr1:
    some instructions
case expr2:
    some other instructions
case expr3:
    some other instructions
default:
    other code
}

8.3.2. fallthrough

  • Go里面switch默认至关于每一个case最后带有break,匹配成功后不会自动向下执行其余case,而是跳出整个switch, 可是可使用fallthrough强制执行后面的case代码。
integer := 6
switch integer {
case 4:
    fmt.Println("The integer was <= 4")
    fallthrough
case 5:
    fmt.Println("The integer was <= 5")
    fallthrough
case 6:
    fmt.Println("The integer was <= 6")
    fallthrough
case 7:
    fmt.Println("The integer was <= 7")
    fallthrough
case 8:
    fmt.Println("The integer was <= 8")
    fallthrough
default:
    fmt.Println("default case")
}

8.3.3. 没有条件的switch

  • 没有条件的 switch 同 switch true 同样。
  • 这一构造使得能够用更清晰的形式来编写长的 if-then-else 链
package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    case t.Hour() < 17:
        fmt.Println("Good afternoon.")
    default:
        fmt.Println("Good evening.")
    }
}

8.4. goto

  • Go有goto语句——请明智地使用它。用goto跳转到必须在当前函数内定义的标签。例如假设这样一个循环
  • 标签名是大小写敏感的
    func myFunc() {
      i := 0
    Here:   //这行的第一个词,以冒号结束做为标签
      println(i)
      i++
      goto Here   //跳转到Here去
    }

9. 函数

9.1. 定义

  • 函数是Go里面的核心设计,它经过关键字func来声明,它的格式以下
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    //这里是处理逻辑代码
    //返回多个值
    return value1, value2
}
  • 函数能够有一个或者多个参数,每一个参数后面带有类型,经过,分隔
  • 函数能够返回多个值
  • 上面返回值声明了两个变量output1和output2,若是你不想声明也能够,直接就两个类型
  • 若是只有一个返回值且不声明返回值变量,那么你能够省略 包括返回值 的括号
  • 若是没有返回值,那么就直接省略最后的返回信息
  • 若是有返回值, 那么必须在函数的外层添加return语句
package main

import "fmt"

// 返回a、b中最大值.
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func main() {
    x := 3
    y := 4
    z := 5

    max_xy := max(x, y) //调用函数max(x, y)
    max_xz := max(x, z) //调用函数max(x, z)

    fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
    fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
    fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它
}

9.2. 函数空返回

  • 以下,函数没有返回值,可是外面add和multiplied已经能够用了,由于在返回的时候初始化了
func SumAndProduct(A, B int) (add int, Multiplied int) {
    add = A+B
    Multiplied = A*B
    return
}

9.3. 函数变参

  • Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了作到这点,首先须要定义函数使其接受变参:
func myfunc(arg ...int) {}
  • arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型所有是int。在函数体中,变量arg是一个int的slice:
for _, n := range arg {
    fmt.Printf("And the number is: %d\n", n)
}

9.4. 传值与传指针

  • 当咱们传一个参数值到被调用函数里面时,其实是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,由于数值变化只做用在copy上。若是传指针,此时参数仍然是按copy传递的,只是copy的是一个指针。
  • 传指针使得多个函数能操做同一个对象。
  • 传指针比较轻量级 (8bytes),只是传内存地址,咱们能够用指针传递体积大的结构体。若是用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。因此当你要传递大的结构体的时候,用指针是一个明智的选择。
  • Go语言中channel,slice,map这三种类型的实现机制相似指针,因此能够直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍须要取地址传递指针)
package main

import "fmt"

//简单的一个函数,实现了参数+1的操做
func add1(a *int) int { // 请注意,
    *a = *a+1 // 修改了a的值
    return *a // 返回新值
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // 应该输出 "x = 3"

    x1 := add1(&x)  // 调用 add1(&x) 传x的地址

    fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
    fmt.Println("x = ", x)    // 应该输出 "x = 4"
}

9.5. defer

  • Go语言中有种不错的设计,即延迟(defer)语句,你能够在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回
    • 本身的理解:按照上面这句话说的,defer必定会是在函数执行到最后,这个最后是函数立刻要结束了,包括提早返回。而后执行defer语句, 再返回要返回的内容
func ReadWrite() bool {
    file.Open("file")
// 作一些工做
    if failureX {
        file.Close()
        return false
    }

    if failureY {
        file.Close()
        return false
    }

    file.Close()
    return true
}

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

func ReadWrite() bool {
    file.Open("file")
    defer file.Close()
    if failureX {
        return false
    }
    if failureY {
        return false
    }
    return true
}
  • 若是有不少调用defer,那么defer是采用后进先出模式
因此以下代码会输出4 3 2 1 0
for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

9.6. 函数的闭包

  • Go函数能够是闭包的。闭包是一个函数值,它来自函数体的外部的变量引用。 函数能够对这个引用值进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。
例如,函数 adder 返回一个闭包。每一个闭包都被绑定到其各自的 sum 变量上。 

package main
import "fmt"
func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}
func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

9.7. 函数做为值、类型

  • 在Go中函数也是一种变量,咱们能够经过type来定义它,它的类型就是全部拥有相同的参数,相同的返回值的一种类型
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
  • 将函数做为参数进行传递,咱们能够实现不少种的逻辑,这样使得咱们的程序变得很是的灵活。
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类型是同样的,可是咱们能够实现不少种的逻辑,这样使得咱们的程序变得很是的灵活。

9.8. Panic和Recover

  • Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。必定要记住,你应当把它做为最后的手段来使用,也就是说,你的代码中应当没有,或者不多有panic的东西。这是个强大的工具,请明智地使用它。
  • Panic:是一个内建函数,能够中断原有的控制流程,进入一个panic状态中。当函数F调用panic,函数F的执行被中断,可是F中的延迟函数会正常执行,而后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中全部调用的函数返回,此时程序退出。panic能够直接调用panic产生。也能够由运行时错误产生,例如访问越界的数组。
  • Recover:是一个内建的函数,可让进入panic状态的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程当中,调用recover会返回nil,而且没有其它任何效果。若是当前的goroutine陷入panic状态,调用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
}

9.9. 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函数。下图详细地解释了整个执行过程:

10. 方法(面向对象)

10.1. 用方法实现类

  • Go没有类。然而,仍然能够在结构体类型上定义方法。
  • 前面咱们介绍了函数和struct,那你是否想过函数看成struct的字段同样来处理呢?今天咱们就讲解一下函数的另外一种形态,带有接收者的函数,咱们称为method
  • 方法接收者 出如今 func 关键字和方法名之间的参数中。

10.2. 自定义类型

  • 实际上只是一个定义了一个类型别名
type ages int

type money float32

type months map[string]int

m := months {
    "January":31,
    "February":28,
    ...
    "December":31,
}

10.3. 任意类型均可以定义方法

  • 任何你自定义的类型、内置类型、struct等各类类型上面
  • 可是,不能对来自其余包的类型或基础类型定义方法
package main
import (
    "fmt"
    "math"
)
type Vertex struct {
    X, Y float64
}
func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
    v := &Vertex{3, 4}
    fmt.Println(v.Abs())
}
package main

import (
    "fmt"
    "math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

func main() {
    f := MyFloat(-math.Sqrt2)
    fmt.Println(f.Abs())
}

10.4. 接收者为指针的方法

  • 首先避免在每一个方法调用中拷贝值(若是值类型是大的结构体的话会更有效率)
  • 其次,方法能够修改接收者指向的值
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 //a slice of boxes

func (b Box) Volume() float64 {
    return b.width * b.height * b.depth
}

func (b *Box) SetColor(c Color) {
    b.color = c
}

func (bl BoxList) BiggestColor() Color {
    v := 0.00
    k := Color(WHITE)
    for _, b := range bl {
        if bv := b.Volume(); bv > v {
            v = bv
            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\n", 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.BiggestColor().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.BiggestColor().String())
}

10.4.1. 指针和非指针的区别

  • 指针做为Receiver会对实例对象的内容发生操做,而普通类型做为Receiver仅仅是以副本做为操做对象,并不对原实例对象发生操做
  • 也就是说:若是一个method的receiver是*T,你能够在一个T类型的实例变量V上面调用这个method,而不须要&V去调用这个method
  • 相似的:若是一个method的receiver是T,你能够在一个T类型的变量P上面调用这个method,而不须要 P去调用这个method

10.5. method继承

  • method也是能够继承的。若是匿名字段实现了一个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
}

//在human上面定义了一个method
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

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

10.6. method重写

  • 上面的例子中,若是Employee想要实现本身的SayHi,怎么办?简单,和匿名字段冲突同样的道理,咱们能够在Employee上面定义一个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
}

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

//Employee的method重写Human的method
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", 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()
}

11. 接口

11.1. interface类型

  • 接口类型是由一组方法定义的集合
  • interface能够被任意的对象实现
  • 一个对象能够实现任意多个interface
type Human struct {
    name string
    age int
    phone string
}

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

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

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

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

//Human对象实现Guzzle方法
func (h *Human) Guzzle(beerStein string) {
    fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}

// Employee重载Human的Sayhi方法
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) //此句能够分红多行
}

//Student实现BorrowMoney方法
func (s *Student) BorrowMoney(amount float32) {
    s.loan += amount // (again and again and...)
}

//Employee实现SpendSalary方法
func (e *Employee) SpendSalary(amount float32) {
    e.money -= amount // More vodka please!!! Get me through the day!
}

// 定义interface
type Men interface {
    SayHi()
    Sing(lyrics string)
    Guzzle(beerStein string)
}

type YoungChap interface {
    SayHi()
    Sing(song string)
    BorrowMoney(amount float32)
}

type ElderlyGent interface {
    SayHi()
    Sing(song string)
    SpendSalary(amount float32)
}

11.2. interface值

  • 简单总结就是,只要是实现了接口的类型均可以赋值给接口
package main

import "fmt"

type Human struct {
    name string
    age int
    phone string
}

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

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

//Human实现SayHi方法
func (h Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", 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\n", e.name,
        e.company, e.phone)
    }

// 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}
    tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}

    //定义Men类型的变量i
    var i Men

    //i能存储Student
    i = mike
    fmt.Println("This is Mike, a Student:")
    i.SayHi()
    i.Sing("November rain")

    //i也能存储Employee
    i = tom
    fmt.Println("This is tom, an Employee:")
    i.SayHi()
    i.Sing("Born to be wild")

    //定义了slice Men
    fmt.Println("Let's use a slice of Men and see what happens")
    x := make([]Men, 3)
    //这三个都是不一样类型的元素,可是他们实现了interface同一个接口
    x[0], x[1], x[2] = paul, sam, mike

    for _, value := range x{
        value.SayHi()
    }
}

11.3. 空interface

  • 空interface(interface{})不包含任何的method,正由于如此,全部的类型都实现了空interface。空interface对于描述起不到任何的做用(由于它不包含任何的method),可是空interface在咱们须要存储任意类型的数值的时候至关有用,由于它能够存储任意类型的数值。
  • 一个函数把interface{}做为参数,那么他能够接受任意类型的值做为参数
  • 若是一个函数返回interface{},那么也就能够返回任意类型的值
// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a能够存储任意类型的数值
a = i
a = s

11.4. 接口类型判断

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

11.4.1. Comma-ok断言

  • Go语言里面有一个语法,能够直接判断是不是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
  • 若是element里面确实存储了T类型的数值,那么ok返回true,不然返回false。
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 // an int
    list[1] = "Hello" // a string
    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\n", index, value)
        } else if value, ok := element.(string); ok {
            fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
        } else if value, ok := element.(Person); ok {
            fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
        } else {
            fmt.Printf("list[%d] is of a different type\n", index)
        }
    }
}

11.4.2. switch测试

  • 这里有一点须要强调的是:element.(type)语法不能在switch外的任何逻辑里面使用,若是你要在switch外面判断一个类型就使用comma-ok
package main

import (
    "fmt"
    "strconv"
)
type Element interface{}
type List [] Element
type Person struct {
    name string
    age int
}
//打印
func (p Person) String() string {
    return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
    list := make(List, 3)
    list[0] = 1 //an int
    list[1] = "Hello" //a string
    list[2] = Person{"Dennis", 70}
    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)
        }
    }
}

11.5. 嵌入interface

  • Go里面真正吸引人的是它内置的逻辑语法,就像咱们在学习Struct时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到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
}

咱们看到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)
}

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

package main
import (
    "fmt"
    "os"
)

type Reader interface {
    Read(b []byte) (n int, err error)
}
type Writer interface {
    Write(b []byte) (n int, err error)
}
type ReadWriter interface {
    Reader
    Writer
}
func main() {
    var w Writer
    // os.Stdout 实现了 Writer
    w = os.Stdout
    fmt.Fprintf(w, "hello, writer\n")
}

11.6. 接口反射

  • 使用reflect通常分红三步,下面简要的讲解一下:要去反射是一个类型的值(这些值都实现了空interface),首先须要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不一样的状况调用不一样的函数)。这两种获取方式以下:
t := reflect.TypeOf(i)    //获得类型的元数据,经过t咱们能获取类型定义里面的全部元素
v := reflect.ValueOf(i)   //获得实际的值,经过v咱们获取存储在里面的值,还能够去改变值
  • 转化为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)

由于传值,传的是拷贝的副本,若是想改变就必须改变原始值

11.7. 隐式接口

  • 类型经过实现那些方法来实现接口。 没有显式声明的必要;因此也就没有关键字“implements“。
  • 隐式接口解藕了实现接口的包和定义接口的包:互不依赖。(也就是说接口能够跨包实现)

11.8. 接口的几个实际应用

11.8.1. Stringers

  • 一个广泛存在的接口是 fmt 包中定义的 Stringer
type Stringer struct {
    String() string
}
package main
import (
    "fmt"
    "strconv"
)

type Human struct {
    name string
    age int
    phone string
}

// 经过这个方法 Human 实现了 fmt.Stringer
func (h Human) String() string {
    return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years -  ✆ " +h.phone+"❱"
}

func main() {
    Bob := Human{"Bob", 39, "000-7777-XXX"}
    fmt.Println("This Human is : ", Bob)
}
  • 这也是实现了fmt.Stringer这个interface,即若是须要某个类型能被fmt包以特殊的格式输出,你就必须实现Stringer这个接口。若是没有实现这个接口,fmt将以默认的方式输出。
  • 实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,所以没必要再定义String()方法了

11.8.2. 错误error

  • 这个地方有一个内建接口的概念,Go 程序使用 error 值来表示错误状态。
type error interface {
    Error() string
}

(与 fmt.Stringer 相似,`fmt` 包在输出时也会试图匹配 `error`。)

一般函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 `nil`, 来进行错误处理。

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
}
fmt.Println("Converted integer:", i)

error 为 nil 时表示成功;非 nil 的 error 表示错误。 

    package main

    import (
        "fmt"
        "time"
    )

    type MyError struct {
        When time.Time
        What string
    }

    func (e *MyError) Error() string {
        return fmt.Sprintf("at %v, %s",
            e.When, e.What)
    }

    func run() error {
        return &MyError{
            time.Now(),
            "it didn't work",
        }
    }

    func main() {
        if err := run(); err != nil {
            fmt.Println(err)
        }
    }

12. 并发

12.1. goroutine

  • goroutine是Go并行设计的核心。goroutine说到底其实就是协程,可是它比线程更小,十几个goroutine可能体如今底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),固然会根据相应的数据伸缩。也正由于如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
  • goroutine是经过Go的runtime管理的一个线程管理器。
  • goroutine经过go关键字实现了,其实就是一个普通的函数。
go f(x, y, z)
  • goroutine 在相同的地址空间中运行,所以访问共享内存必须进行同步。sync 提供了这种可能,不过在 Go 中并不常常用到,由于有其余的办法
  • 不过设计上咱们要遵循:不要经过共享来通讯,而要经过通讯来共享。

12.2. channel(信道)

  • goroutine运行在相同的地址空间,所以访问共享内存必须作好同步。那么goroutine之间如何进行数据的通讯呢,Go提供了一个很好的通讯机制channel。channel能够与Unix shell 中的双向管道作类比:能够经过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也须要定义发送到channel的值的类型。注意,必须使用make 建立channel channel 是有类型的管道,能够用 channel 操做符 <- 对其发送或者接收值。
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
  • channel经过操做符<-来接收和发送数据
ch <- v    // 发送v到channel ch.
v := <-ch  // 从ch中接收数据,并赋值给v

12.2.1. 无缓冲信道

  • 无缓冲的信道永远不会存储数据,只负责数据的流通,为何这么讲呢?

    • 从无缓冲信道取数据,必需要有数据流进来才能够,不然当前线阻塞
    • 数据流入无缓冲信道, 若是没有其余goroutine来拿走这个数据,那么当前线阻塞
  • 默认状况下,channel接收和发送数据都是阻塞的,除非另外一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不须要显式的lock。所谓阻塞,也就是若是读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。☆☆☆☆☆无缓冲channel是在多个goroutine之间同步很棒的工具☆☆☆☆☆

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)
}
  • 由上可知信道有阻塞主线程的做用,也就是无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另外一端已经准备好
var ch chan int = make(chan int)

func foo() {
    ch <- 0  // 向ch中加数据,若是没有其余goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走
}

func main() {
    go foo()
    <- ch // 从ch取数据,若是ch中还没放数据,那就挂起main线,直到foo函数中放数据为止
}

12.2.2. 死锁

  • 总结来看,为何会死锁?非缓冲信道上若是发生了流入无流出,或者流出无流入,也就致使了死锁。或者这样理解 Go启动的全部goroutine里的非缓冲信道必定要一个线里存数据,一个线里取数据,要成对才行
死锁1
func main() {
    ch := make(chan int)
    <- ch // 阻塞main goroutine, 信道c被锁
}
死锁2
var ch1 chan int = make(chan int)
var ch2 chan int = make(chan int)

func say(s string) {
    fmt.Println(s)
    ch1 <- <- ch2 // ch1 等待 ch2流出的数据
}

func main() {
    go say("hello")
    <- ch1  // 堵塞主线
}

12.3. Buffered Channels(缓冲 channel)

  • Go也容许指定channel的缓冲大小,很简单,就是channel能够存储多少元素。ch:= make(chan bool, 4),建立了能够存储4个元素的bool 型channel。在这个channel 中,前4个元素能够无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其余goroutine从channel 中读取一些元素,腾出空间。
ch := make(chan type, value)
当 value = 0 时,channel 是无缓冲阻塞读写的,当value > 0 时,channel 有缓冲、是非阻塞的,直到写满 value 个元素才阻塞写入
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)
}
        //修改成1报以下的错误:
        //fatal error: all goroutines are asleep - deadlock!

12.4. range和close

  • 上面这个例子中,咱们须要读取两次c,这样不是很方便,Go考虑到了这一点,因此也能够经过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)
    }
}

for i := range c可以不断的读取channel里面的数据,直到该channel被显式的关闭(这句话说明这个循环会一直存在,直到你调用了close)。上面代码咱们看到能够显式的关闭channel,生产者经过内置函数close关闭channel。关闭channel以后就没法再发送任何数据了,在消费方能够经过语法v, ok := <-ch测试channel是否被关闭。若是ok返回false,那么说明channel已经没有任何数据而且已经被关闭。
  • 记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引发panic
  • 另外记住一点的就是channel不像文件之类的,不须要常常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的

12.5. select

  • 咱们上面介绍的都是只有一个channel的状况,那么若是存在多个channel的时候,咱们该如何操做呢,Go里面提供了一个关键字select,经过select能够监听channel上的数据流动。
  • select默认是阻塞的,只有当监听的channel中有发送或接收能够进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。
    • (也就是用了select以后就一直处于监听状态了,只要有管道的读写操做就会执行select,由于select被包裹在for循环中了)
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)
}
  • 在select里面还有default语法,select其实就是相似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select再也不阻塞等待channel)
select {
case i := <-c:
    // use i
default:
    // 当c阻塞的时候执行这里
}

12.6. 超时

  • 有时候会出现goroutine阻塞的状况,那么咱们如何避免整个程序进入阻塞的状况呢?咱们能够利用select来设置超时,经过以下的方式实现:
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
            }
        }
    }()
    <- o
}

12.7. runtime goroutine

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

  • Goexit:退出当前执行的goroutine,可是defer函数还会继续调用
  • Gosched:让出当前goroutine的执行权限,调度器安排其余等待的任务运行,并在下次某个时候从该位置恢复执行。
  • NumCPU:返回 CPU 核数量
  • NumGoroutine:返回正在执行和排队的任务总数
  • GOMAXPROCS:用来设置能够并行计算的CPU核数的最大值,并返回以前的值。

13. 免责说明

  • 本文档中的部份内容摘自网上的众多博客,仅做为本身知识的补充和整理,并分享给其余须要的coder,不会用于商用。
  • 由于不少博客的地址看完没有及时作保存,因此不少不会在这里标明出处,很是感谢各位大牛的分享,也但愿你们理解。
  • 若是原文做者感受不适,能够及时联系我shiguoqing999@163.com,我将及时删除争议部份内容

14. 追责声明

  • 若有大段引用超过全文50%的内容,请在文档结尾标明原文出处:龙马行空-石国庆-朱庇特-https://my.oschina.net/u/1416844/blog,不然将视为抄袭,予以法律追究,请各位尊重我的知识产权。
相关文章
相关标签/搜索