在之前需要帮助身边同事快速入门go语言,整理过一份简单的入门文档。现下贴出来仅供有需要的同学参考

学习资料

  • 入门书籍

了解Golang

什么是Go

Go(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言,被称为云计算时代的C语言。

Go语言的优势

  • Go语言最大的优势是在于其语言层面就支持并发,并且使用Go编译的程序的运行速度可以媲美C或C++的速度,同时更加安全。
  • 可以直接编译成机器码,运行时不依赖其他库(dll/so),部署就是放一个可执行文件到服务器就可以了。
  • Go是静态类型语言,在编译时就能检查出大多数问题,但是有动态语言的感觉。
  • 内置runtime,支持垃圾回收。
  • 简单易学,有C语言的基因。
  • 内置有丰富的标准库,特别是网络库非常强大。
  • 内置强大的工具,例如gofmt工具可以格式化代码,方便团队review。
  • 跨平台编译,如果代码中不包含cgo就可以做到在Windows系统下编译Linux应用
  • 内嵌C语言支持,可以直接包含C代码。

Go语言的使用场景

  • 服务器编程,例如处理日志、数据打包、虚拟机处理、文件系统等。
  • 分布式系统、数据库代理服务器等。
  • 网络编程,包括Web应用、API应用、下载应用。
  • 内存数据库。
  • 云平台。

Go语言知名开源项目

  • ....

安装Go

使用Goland安装go(推荐)

Goland -> preferences -> Go -> GOROOT, 点击右边加号,然后选择合适的版本即可自动安装go

Mac中通过源码安装go

# 选择对应架构的go源码下载
# 无特殊情况默认选择最行正式版本
wget https://go.dev/dl/go1.17.6.darwin-arm64.tar.gz
# 解压到指定目录,以~/golang为例
mkdir $HOME/golang
tar -zxvf go1.17.6.darwin-arm64.tar.gz -C $HOME/golang/
# 修改环境变量
echo "export GOROOT=$HOME/golang/go" >> ~/.zshrc
echo "export GOPATH=$HOME/golang/path" >> ~/.zshrc
echo "export PATH=$PATH:$HOME/golang/go/bin" >> ~/.zshrc

环境变量

通过

go env

查看go的详细信息


❯ go env
GO111MODULE=""
GOARCH="arm64"
GOBIN=""
GOCACHE="/Users/lvjinyu/Library/Caches/go-build"
GOENV="/Users/lvjinyu/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="arm64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/lvjinyu/go/pkg/mod"
GONOPROXY="gitlab.com"
GONOSUMDB="gitlab.com"
GOOS="darwin"
GOPATH="/Users/lvjinyu/go"
GOPRIVATE="gitlab.com"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_arm64"
GOVCS=""
GOVERSION="go1.17.3"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/dev/null"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/jj/zrtwy2v55mn0p_xx11ynp5lw0000gn/T/go-build185738076=/tmp/go-build -gno-record-gcc-switches -fno-common"

如果需要修改默认的环境变量配置修改

vim ~/.bash_profile

vim ~/.zshrc

, 示例如下


#GOPATH root bin
export GOBIN=$GOROOT/bin
export PATH=$PATH:$GOBIN
#GOPATH
export GOPATH=$HOME/go
#GOPATH bin
export PATH=$PATH:$GOPATH/bin

标准命令详解

❯ go --help
Go is a tool for managing Go source code.
Go是用于管理Go源代码的工具。

Usage用法:
  go command [arguments]

The commands are:

  build 命令用于编译我们指定的源码文件或代码包以及它们的依赖包。
    -o 指定输出的文件名,可以带上路径,例如 go build -o a/b/c
    -i 安装相应的包,编译+go install
    -a 更新全部已经是最新的包的,但是对标准包不适用
    -n 把需要执行的编译命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行的
    -p n 指定可以并行可运行的编译数目,默认是CPU数目
    -race 开启编译的时候自动检测数据竞争的情况,目前只支持64位的机器
    -v 打印出来我们正在编译的包名
    -work 打印出来编译时候的临时文件夹名称,并且如果已经存在的话就不要删除
    -x 打印出来执行的命令,其实就是和-n的结果类似,只是这个会执行
    -ccflags 'arg list' 传递参数给5c, 6c, 8c 调用
    -compiler name 指定相应的编译器,gccgo还是gc
    -gccgoflags 'arg list' 传递参数给gccgo编译连接调用
    -gcflags 'arg list' 传递参数给5g, 6g, 8g 调用
    -installsuffix suffix 为了和默认的安装包区别开来,采用这个前缀来重新安装那些依赖的包,-race的时候默认已经是-installsuffix race,大家可以通过-n命令来验证
    -ldflags 'flag list' 传递参数给5l, 6l, 8l 调用
    -tags 'tag list' 设置在编译的时候可以适配的那些tag,详细的tag限制参考里面的http://golang.org/pkg/go/build/
  clean       删除掉执行其它命令时产生的一些文件和目录。
    -i 清除关联的安装的包和可运行文件,也就是通过go install安装的文件
    -n 把需要执行的清除命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行的
    -r 循环的清除在import中引入的包
    -x 打印出来执行的详细命令,其实就是-n打印的执行版本
  doc 命令可以打印附于Go语言程序实体上的文档。
  env 用于打印Go语言的环境信息。
  bug 启动错误报告。
  fix 把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。
  fmt 在包源上运行gofmt。
    -l 显示那些需要格式化的文件
    -w 把改写后的内容直接写入到文件中,而不是作为结果打印到标准输出。
    -r 添加形如“a[b:len(a)] -> a[b:]”的重写规则,方便我们做批量替换
    -s 简化文件中的代码
    -d 显示格式化前后的diff而不是写入文件,默认是false
    -e 打印所有的语法错误到标准输出。如果不使用此标记,则只会打印不同行的前10个错误。
    -cpuprofile 支持调试模式,写入相应的cpufile到指定的文件
  generate 通过处理源生成Go文件。
  get 下载或更新安装指定的代码包及其依赖包,并对它们进行编译和安装。
    -d 只下载不安装
    -f 只有在你包含了-u参数的时候才有效,不让-u去验证import中的每一个都已经获取了,这对于本地fork的包特别有用
    -fix 在获取源码之后先运行fix,然后再去做其他的事情
    -t 同时也下载需要为运行测试所需要的包
    -u 强制使用网络去更新包和它的依赖包
    -v 显示执行的命令
  install 用于编译并安装指定的代码包及它们的依赖包。
  list 列出指定的代码包的信息。
  run 命令可以编译并运行命令源码文件。
  test 对Go语言编写的程序进行测试。
    -bench regexp 执行相应的benchmarks,例如 -bench=.
    -cover 开启测试覆盖率
    -run regexp 只运行regexp匹配的函数,例如 -run=Array 那么就执行包含有Array开头的函数
    -v 显示测试的详细命令
  tool 运行指定的go工具
    go tool fix . 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1,例如API的变化
    go tool vet directory|files 用来分析当前目录的代码是否都是正确的代码,例如是不是调用fmt.Printf里面的参数不正确,例如函数里面提前return了然后出现了无用代码之类的。
  version 打印Go的版本信息
  vet 用于检查Go语言源码中静态错误的简单工具。

Use "go help [command]" for more information about a command.

Additional help topics:

  c           calling between Go and C
  buildmode   description of build modes
  filetype    file types
  gopath      GOPATH environment variable
  environment environment variables
  importpath  import path syntax
  packages    description of package lists
  testflag    description of testing flags
  testfunc    description of testing functions

Use "go help [topic]" for more information about that topic.

第一个Go程序

hello_world.go

package main

import "fmt"

func main(){
    fmt.Println("hello world!")
}

执行

go run hello_world.go

或通过

go build hello_world.go

将程序编译成二进制文件,然后执行

./hello_world

运行,可以输出

hello world!


编程基础

内置关键字

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

预定义标识符

append  bool    byte    cap     close  complex complex64 complex128 uint16
copy    false   float32 float64 imag   int     int8      int16      uint32
int32   int64   iota    len     make   new     nil       panic      uint64
print   println real    recover string true    uint      uint8      uintptr

注释方法

// 单行注释

/*
多行注释
*/

标识符

  • 由字母、下划线、数字组成,不能以数字开头、不能是关键字、区分大小写
  • 标识符必须在使用前都必须先声明
  • 在一个代码块中,不允许重复声明同一个标识符

可见性规则

Go语言中约定使用 大小写 来决定常量、变量、类型、接口、结构或函数是否可以被外部包所调用 函数名字首字母 小写 即为

private

私有的 函数名字首字母 大写 即为

public

公有


包引用 import

import ff "fmt" // 包别名写法
import "os"
import "io"
// 简写如下
import (
    ff "fmt" // 包别名写法
    "os"
    "io"
)

基本类型

基本数据类型

类型名称字节数零值说明
package main
import "fmt"

func main() {
    // 声明变量的格式为 `var 变量名1, 变量名2, ... 类型`
    var a int // 声明变量但没有初始化,默认初始化值为0
    var b int = 10 // 初始化变量声明
    var c = 20 // 初始化变量声明,会自动推导类型
    d := 20.0 // 短变量声明运算符`:=`,会自动推导类型
    _, e, f := 20, 30, 40 // 多重赋值, _ 为匿名变量,类似/dev/null
    // go 语言中不允许声明未使用的变量,因此需要使用匿名变量占位
    fmt.Println(a, b, c, d, e, f)
    a = 5 // 变量赋值
    fmt.Println(a)
}

常量声明

package main
import "fmt"

func main() {
    const (
        x = 10 // 自动推导类型
        y = 12.1
        z // 这里比较特殊,实际值和上一行一样是12.1
    )
    fmt.Println(x, y, z)
    // iota 常量枚举,在代码中每隔一行自动累加1
    const (
        a = iota // 0
        b = iota // 1
        c = iota // 2
    )
    fmt.Println(a, b, c)
    // iota每次遇到const就会重置为0
    cost d = iota // 0
    fmt.Println(d)
    // iota可以只写一次
    const (
        e = iota // 0
        f // 1
        g // 2
    )
    fmt.Println(e, f, g)
    // 如果在同一行内则值一样
    const (
        h = iota // 0
        i, j, k = iota, iota, iota // 都为1
        l = iota // 2
    )
    fmt.Println(h, i, j, k, l)
    const (
        m = iota // 0
        n = "a" // n为独立值"a", 但仍然需要进行iota += 1
        o = 100 // o为独立值100, 但仍然需要进行iota += 1
        p = iota // 3, 恢复计数
    )
    fmt.Println(m, n, o, p)
}

fmt包的格式化输入输出

  • %c,字符型,可以把合法的整数按照Unicode码表转换成字符输出
  • %d,十进制整形
  • %o,八进制整形
  • %f,浮点数
  • %e/%E,科学计数法表示的浮点数
  • %x/%X,十六进制整数
  • %s,字符串,输出字符串的字符直到空字符'\0'为止
  • %T,Go语言中的数据类型
  • %v,使用默认格式输出

运算符

  • 算数运算符:+、-、*、/、%,注意Go语言中自增自减运算符++和--只能后置不能前置
  • 关系运算符:>、<、==、>=、<=,!=
  • 逻辑运算符:&&、||、!
  • 位运算符:&、|、^、<<、>>
  • 取地址运算符&
    和取值运算符*

流程控制

选择结构(if、switch)

// if选择
func testIf() {
    s := 92
    // Go的判断条件不需要加括号
    if s >= 80 {
        fmt.Println("成绩优秀")
    }

    // if支持写入一个初始化语句,初始化语句和判断条件用分号分隔
    if t := 75; t >= 80 {
        fmt.Prinln("成绩优秀")
    } else if t >= 60 {
        fmt.Println("成绩不佳")
    } else {
        fmt.Println("不及格")
    }
}

// switch选择
func testSwitch() {
    sex := 'male'
    switch sex {
        case "male":
            fmt.Println("左转")
            break // switch语句中每个case结束默认break,因此可以不用写
        case "female":
            fmt.Println("右转")
            fallthrough // 如果使用了该关键字并能被执行到,下面的所有case都会无条件执行
        default:
            fmt.Println("直走")
    }

    // 与if类似可以在switch初始化
    switch age := 24; age {
        case 10, 20:
            // some actions
        // ...
    }

    // switch 也可以没有目标变量,这时要在case中放条件
    score := 81 
    switch {
        case score >= 80:
            fmt.Println("成绩优秀")
        case score >= 80:
            fmt.Println("成绩良好")
        default:
            fmt.Println("不及格")
    }
}

循环结构

go语言中没有while或者do...while关键字,只有for循环和range迭代器
func testFor() {
    sum := 0
    // 和if语句一样,同样不需要括号
    for i := 1; i <= 100; i++ {
        sum += 1
    }
    fmt.Println("sum = ", sum)

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

    // range 默认返回两个值,元素的位置和元素本身
    // 如果不需要第二个返回值可以写为i := 或者i, _ :=
    for i, data := range str { 
        fmt.Printf("str[%d] is %c\n", i. data)
    }
}

函数

自定义函数

// 无参数、无返回值的函数
func testFunc1() {
    fmt.Println("Hello World!")
}

// 这里的abc称为形参
func testFunc2(a, b int, c string) {
    fmt.Printf("a = %d, b = %d, c is %s\n", a, b, c)
}

// 不定参数,其本质是切片,只能放在形参列表的最后
func testFunc3(args... int) {
    fmt.Println("len(args) = ", len(args))
    for i := 0; i < len(args); i++ {
        fmt.Printf("args[%d] = %d\n", i, args[i])
    }

    for i, value := range args {
        fmt.Printf("args[%d] = %d\n", i. value)
    }
}

// 只有1个返回值
func testFunc4() int {
    return 666
}

// 多个 返回值
func testFunc5() (int, int, int) {
    return 1, 2, 3
}

func testFunc6() (a, b, c int) {
    a, b, c = 1, 2, 3
    return
}

函数类型

在Go语言中,函数也是一种数据类型,同样可以使用

type

关键字来定义。它的类型就是拥有相同参数、相同返回值的一种类型。使用自定义的函数类型我们可以实现多态与函数回调


package main
import "fmt"

// 定义一种整数二则运算的类型
type FuncType func(int, int) int

func main() {
    var (
        o1, o2 int
        f FuncType
    )

    o1, o2 = 5, 3
    f = MyMod // f可以是任何满足FuncType定义的函数,这就是一种多态的体现

    // 直接写result := f(operand1, operand2)也行
    // 这里写一种回调的写法
    result := InvokeCalculation(o1, o2, f)
    fmt.Println("result = ", result)
}

func InvokeCalculation(o1, o2 int, f FuncType) int {
    fmt.Println("Invoking function f...")
    return f(o1, o2)
}

func MyAdd(o1, o2 int) int {
    return o1 + o2
}

func MyMod(o1, o2 int) int {
    return o1 % o2
}

延迟调用defer

Go中的defer类型C++中的析构函数,用于在调用结束前做一些收尾清理的工作,例如关闭文件、网络连接等

func main() {
    defer fmt.Println("closing...")
    fmt.Println("working...")
}

复合类型

指针

指针代表某个内存地址的值,Go语言对于指针的支持介于Java和C/C++之间,没有像Java一样取消了对指针的直接操作能力,也避免了C/C++中由于对指针的滥用而造成的安全性和可靠性问题。

func main() {
    var a int = 10
    fmt.Printf("a = %d\n", a) //变量a的内容
    fmt.Printf("&a = %v\n", &a) //变量a在内存中的首地址
}

为了保存某个变量的地址,需要指针类型。例如要保存

int

类型变量的地址需要

*int

、要保存

*int

类型变量的地址需要

**int

。当指针类型为nil时即没有合法指向时,不要通过*p的方式进行操作,否则会出现nil pointer dereference,即空指针间接引用异常。


func main() {
    var a int = 10
    var p *int //p是指针,此时还没有指向a,默认值为nil
    p = &a
    fmt.Printf("p = %v\n", p)

    *p = 666 //*p操作的不是指针类型变量p的内存,而是p所指向的内存(即a)
}

除了

*p = &a

这种指向方式,还可以使用

new(T)

函数。该函数将创建一个T类型的匿名变量,准确来说是为该类型分配一块新的内存空间,并返回该内存空间的地址,我们可以用指针类型

*T

来接收这个地址。new()函数类似于C语言中的动态申请空间,不过Go语言有内存回收机制,所以开发者只需要申请不需要手动回收内存


func main() {
    var p1 *int
    p1 = new(int) //p1指向匿名的int类型变量

    p2 := new(int)
    *p2 = 111
}

另外,由于Go语言在进行函数调用时使用的是值传递,那么假如我要编写一个函数交换两个整形数的值

func main() {
    a, b := 1, 2
    Swap(a, b)
    fmt.Printf("In main: %d, %d\n", a, b)
    TrueSwap(&a, &b) //取地址传给指针类型的形参
    fmt.Printf("In main: %d, %d\n", a, b)
}

// 该方式仅仅在函数作用域内交换了两个形参的值
// 但是外部调用的函数中变量a、b的值没有改变
func Swap(a, b int) {
    a, b = b, a
    fmt.Printf("In Swap: %d, %d\n", a, b)
}

// 需要通过指针来操作原变量内存地址处的内容
func TrueSwap(a, b *int){
    *a, *b = *b, *a
    fmt.Printf("In Swap: %d, %d\n", *a, *b)
}

数组

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。数组的长度必须是常量(不可以是变量),且是类型的组成部分,[2]int和[3]int是不同的类型。

func main() {
    // 和变量一样如果直接赋值则前面的类型可以省略
    var array [3]int = [3]int{1, 2, 3} 
    //array := [3]int {1, 2, 3}
    fmt.Println("array = ", array)

    //Go中可以对数组进行部分初始化,未初始化的元素为默认值
    array2 := [5]int{1, 2, 3} //第0~2个元素被初始化,第3~4个元素默认为0
    fmt.Println("array2 = ", array2)

    //Go中还可以指定某个位置的元素初始化
    array3 := [5]int{2: 10, 4: 20} //只初始化了下标为2和4的元素
    fmt.Println("array3 = ", array3)

    // 多维数组的使用
    var arr [3][4]int
    count := 0

    for i := 0; i < 3; i++ {
        for j := 0; j < 4; j++ {
            arr[i][j] = count
            count++
        }
    }

    fmt.Println(arr)

    arr2 := [3][4]int{{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}} //初始化
    arr3 := [3][4]int{{0, 1, 2}, {4, 5, 6}, {8, 9}} //部分初始化
    arr4 := [3][4]int{{0, 1, 2}, {4, 5, 6}} //部分初始化
    arr5 := [3][4]int{1: {4, 5, 6, 7}} //指定初始化第1行
    arr6 := [3][4]int{1: {1: 5, 3: 7}} //指定初始化第1行中的第1、3列
    //使用这些数组...
}

切片(slice)

数组的长度在定义后无法修改,并且数组传递是值传递,每次传递都将产生一份副本。Go语言提供了切片来弥补数组的不足。切片并不是数组或数组指针,它通过内部指针和相关属性引用数组片段,以实现变长方案。

func main() {
    // 定义切片可以由数组得来
    array := [5]int{10, 20, 30, 40, 50}
    // array[low:high:max]
    slice1 := array[0:3:5] // 起始位置为0,终止位置为3(左闭右开),最大终止位置为5(所以容量为5-0)
    // 如果不指定容量就和长度一样
    slice2 := array[0:3] // 取数组第0、1、2位置的元素
    slice3 := array[:] // 取数组全部元素
    slice4 := array[0] // 取数组第0个元素
    slice5 := array[:3] // 同array[0:3]
    slice6 := array[2:] // 从第2位到末尾

    // 上面的切片长度和容量是固定的,也可以直接定义不定长的切片
    slice := []int{} //中括号内不指定数字或写为`...`就表示这是一个变长的切片
    slice = append(slice, 11) // 给切片追加一个成员

    s2 := make([]int, 5, 10) // 参数分别为切片类型,长度,容量(也可以只给出类型和数字,不分别指定长度和容量)
}
可以了解下append时遇到切片容量不够时是如何进行扩容的

map

Go语言中的map和很多语言一样,是一种键值对的数据结构,键唯一,map可以用基本数据类型作为键值

func main() {
    var m1 map[int]string // 空的,len(m1)=0
    // 也可以通过make()函数来创建
    m2 := make(map[int]string)
    m3 := make(map[int]string, 10) //指定初始长度

    // 操作map中的元素时可以使用map[key] = value的形式
    m3[1] = "mike" //已经存在该key则修改,不存在则追加
    m3[-13] = "wallace"

    // 在定义map时就可以对其进行初始化
    m4 := map[int]string(1:"mike", -13:"wallace")

    // map的迭代遍历,要注意结果是无序的
    m := map[int]string{1:"mike", 2:"wallace"}
    for key, value := range m {
        fmt.Println("key is:", key, "value is:", value)
    }

    // 判断key是否存在,并且可以利用map来当集合使用
    value, ok := m[3]
    if _, ok := m[3]; !ok {
        fmt.Println("key does not exist!")
    }
}

结构体

在Go中可以使用结构体将不同类型的数据组合成一个整体,结构体定义语法如下:

type Student struct {
    id int 
    name string
    sex byte
    age int
}

func main() {
    // 顺序初始化,每个成员必须初始化
    var s1 Student = Student{1, "mike", 'm', 18}
    fmt.Println("s1 = ", s1)

    // 指定成员初始化,没有初始化的自动赋值为0
    s2 := Student{name: "mike", age: 18}
    fmt.Println("s2 = ", s2)
    // 操作结构体的成员时,要使用.运算符
    fmt.Println("s2.name = ", s2.name)
}

实现面向对象特性

Go以简洁的方式支持了面向对象编程,但并没有传统面向对象编程中的诸多概念,可以通过别的方式实现这些特性: 继承:通过方法实现 封装:通过匿名字段实现 * 多台:通过接口实现

匿名组合(继承)

//人
type Person struct {
    name string
    sex byte
    age int
}

//学生
type Student struct {
    Person //匿名字段,只有类型没有名字。学生包含了人的所有字段,实现了继承
    id int
    addr string
}

//我们来使用学生结构体
func main() {
    var s1 Student = Student{Person{"mike", 'm', 18}, 1, "nanjing"}
    fmt.Printf("s1: %+v\n", s1)
    // 可以直接使用匿名字段中的成员
    fmt.Println(s1.name)
}

方法(封装)

对象有时需要对外提供可以操作的方法(或者说接口)。和普通函数不同,在定义方法时还需要在函数名前加上要绑定的类型

type myint int

//这里的obj叫做接收者,接收者就是传递的一个参数
func (obj myint) Add(num myint) myint { //这时myint类型的对象就有了这样的方法
    return obj + num
}

func main() {
    var a myint = 2
    result := a.Add(3)
    fmt.Println("result = ", result)
}

可以给任意自定义类型(基本类型、结构体)添加方法,但如果自定义的类型本身就是指针类型那么就不能添加方法。一个类型的方法集指的是可以被该类型的值所调用的所有的方法的集合。

type Person struct {
    name string
    sex byte
    age int
}

//值传递,只是一份拷贝
func (p Person) ShowInfo() {
    fmt.Println("Person info:", p)
}

//引用传递,可以修改
func (p *Person) SetInfo(n string, s byte, a int) {
    p.name = n
    p.sex = s
    p.age = a
}

func main() {
    p1 := Person{"mike", 'm', 18}
    p1.ShowInfo()

    var p2 Person
    (&p2).SetInfo("wallace", 'm', 20)
    p2.ShowInfo()
}

方法的继承:在前面的例子中,Student继承了Person,加入我们有绑定Person类型的方法func (p *Person) ShowInfo(){},那么Student类型的变量也可以调用这个方法,这就是方法的继承。

方法的重写:如果Student类型也绑定了一个同名同参数的方法func (p *Student) ShowInfo(){},那么这就是方法的重写,或者说覆盖。那么通过s.ShowInfo()调用的根据就近原则就会优先调用重写的方法。此外,可以显示地通过s.Person.ShowInfo()来调用继承的方法。

接口(多态)

Go中接口有以下特点: 接口明明习惯以er接口 接口只有方法声明,没有实现,没有数据字段 * 接口可以匿名嵌入其他接口,或者嵌入到结构体中

type Manner interface {
    SayHi()
}

type Student struct {
    name string
    id int
}

type Teacher struct {
    name string
    id int
    division int
}

func (s *Student) SayHi() {
    fmt.Println("My name is: ", s.name)
}

func (t *Teacher) SayHi() {
    fmt.Printf("My name is: %s, my division number is: %d\n", t.name, t.division)
}

func main() {
    //定义接口类型的变量,只要是实现了此接口方法的类型,那么这个类型的变量就可以给i赋值
    var i Manner

    s := &Student{"mike", 201}
    i = s
    i.SayHi()

    t := &Teacher{"judy", 102, 3}
    i = t
    i.SayHi()

    ShowManner(s)
    ShowManner(t)

    x := make([]Manner, 3)
    x[0] = s
    x[1] = t
    for _, implementation := range x {
        implementation.SayHi()
    }
}

func ShowManner(i Manner) {
    //some codes..
    i.SayHi()
}

并发编程

常见问题:并行和并发的区别。 并行:同一微观时刻有多条指令在多个处理器上执行 并发:同一时刻一个处理器上只能有一条指令在执行,但多个任务队列交替使用该处理器,在宏观上显得是多任务同时执行 Go语言在语言层面就支持了并发。其相关的API是基于CSP(communicating sequential processes,顺序通信进程)模型,这意味着显式的锁是可以避免的,从而简化了程序的编写。

goroutine

goroutine(协程)是比线程更小的单位,十几个goroutine可能体现在操作系统底层就是五六个线程,Go语言内部实现了goroutine之间的内存共享,执行之只需要极少的栈内存。使用

go

关键字就可以简单的运行一个goroutine。程序启动时,其主函数main在一个单独的goroutine中运行,称为main goroutine。


package main

import (
    "fmt"
    "time"
)

func newTask() {
    for {
        fmt.Println("This is a new task.")
        time.Sleep(time.Second)
    }
}

func main() {
    go newTask() // 新建携程执行该任务,之后主协程继续往下执行
    go func() {
        // 也可以调用匿名函数
    }()

    for {
        fmt.Println("This is main goroutine.")
        time.Sleep(time.Second)
    }

    // 主协程退出时,其他子协程也要跟着退出
    // 开发过程中一般需要注意不要让子协程还没来得及运行主协程就退出
}

channel

并发编程中一个常见的问题是资源竞争问题,为此需要一定的同步机制。goroutine奉行通过通信来共享内存(和同步),而不是共享内存来通信。引用类型channel是CSP模式的具体实现,用于goroutine通讯,其内部实现了同步,确保并发安全。和map类似,channel也是通过make创建的对于底层数据结构的引用。channel传参是引用传递,创建方式:

make(chan Type) // Type可以是int、string等任意类型,无缓冲区
make(chan Type, capacity) // 指定缓冲区容量,如果为0则和上面等价

channel通过操作符

<-

来接收和发送数据,语法为:


channel <- value // 发送value到channel
<- channel // 接收并将其丢弃
x := <- channel // 接收channel中的数据并赋值给x
x, ok := <- channel // 功能同上,同时检查通道是否已关闭或者是否为空

无缓冲区的channel默认是阻塞的,只有向其中发送了内容后才能从中接收数据。

package main

import (
    "fmt"
    "time"
)

var ch = make(chan int)

//公共资源打印机
func Printer(str string) {
    for _, data := range str {
        fmt.Printf("%c", data)
        time.Sleep(time.Second)
    }
    fmt.Printf("\n")
}

func user1() {
    Printer("hello")
    ch <- 0 //向管道发送数据
}

func user2() {
    <- ch //从管道取数据,如果没有数据该goroutine就会阻塞
    Printer("world")
}

func main() {
    go user1()
    go user2()

    for {
        //不让主进程结束的死循环
    }
}

无缓存的channel是同步的,会阻塞到发送者准备好和接收者也准备好。有缓存的通道是异步的,通道空时,接收者被阻塞;通道满时,发送者被阻塞。不需要再写数据时,可以通过close(ch)来关闭channel,这时x, ok := <- ch得到的ok值就为false,那么读数据的进程就可以检查到管道关闭。关闭channel之后再向其发送数据会导致panic。除了在循环中检测ok状态来确定是否结束循环外,还有一个简单的方法可以在管道关闭时结束循环:

// channel关闭时会自动跳出循环
for num := range ch { 
    fmt.Println("num = ", num)
}

单向管道,指定管道只用于读或只用于写,常用场景为生产者消费者模式下生产者只写,消费者只读。

var ch1 chan<- int //只能用于写入int类型的数据,不能读
var ch2 <-chan int //只能从中读int类型数据,不能写

//双向channel可以隐式转换为单向,单向channel无法转为双向
var ch3 chan<- int = ch
var ch4 <-chan int = ch

select

Go提供了关键字select,通过select可以监听channel上的数据流动。select的用法和switch有些类似,但select最大的一条限制就是其下的case语句里必须是一个IO操作

select {
    case <- chan1:
        // 如果成功从chan1读取到数据,则执行该语句
    case chan2 <- 1:
        // 如果成功向chan2写入了1,则执行该语句
    default:
        // 如果上面都没有成功,则进入默认处理流程
}

select会按顺序评估有没有语句可以成功执行,如果没有任意一条语句可以执行(所有都被阻塞)那么这时:1)如果给出了default语句则执行之;2)如果没有给出default语句则select语句将被阻塞,直到至少有一个通信可以进行下去。