1 Star 1 Fork 2

mitslyj / Golang

forked from 极简美 / Golang_tutorial 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
Golang.md 52.29 KB
一键复制 编辑 原始数据 按行查看 历史
极简美 提交于 2021-05-25 10:04 . update Go入门指南/Golang.md.

Go入门指南笔记

在线书籍链接:http://books.studygolang.com/the-way-to-go_ZH_CN/

Go提倡通过接口来针对面向对象编程,通过goroutinechannel来支持并发和并行编程。

Go语言有一个被称之为 “没有废物” 的宗旨,就是将一切没有必要的东西都去掉,不能去掉的就无底线地简化,同时追求最大程度的自动化。他完美地诠释了敏捷编程的KISS秘诀:短小精悍!

第一部分:学习Go语言

一、Go语言的发展目标

  • Go语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行。因此,Go 语言是一门类型安全和内存安全的编程语言。
  • Go语言的另一个目标是对于网络通信、并发和并行编程的极佳支持,从而更好地利用大量的分布式和多核的计算机。实现了分段栈增长和goroutine在线程基础上多路复用技术的自动化。

二、Go语言的特性

  • Go语言从本质上(程序和结构方面)来实现并发编程;
  • Go语言通过接口(interface)来实现多态性,不支持类和继承的概念;
  • Go语言有一个清晰易懂的轻量级类型系统,在类型之间没有层级之说;
  • Go语言使用静态类型,因此是类型安全的;它也是编译型语言,因此程序执行速度也非常快;
  • Go语言不允许隐式的类型转换(原则:让所有的东西都是显示的);
  • Go语言不支持函数重载和操作符重载;
  • Go语言还支持运行时进行反射;
  • Go语言还支持垃圾回收机制,将内存管理从开发人员收回到语言内部;
  • Go语言一个非常重要的特性是其构建速度(编译和链接到机器代码的速度)。依赖管理也是现今软件开发的一个难题,Go语言使用包模型,通过严格的依赖关系检查机制,不仅加快了程序构建速度,还提供了非常好的可测量性。

三、Go环境变量

  • $GOROOT:表示Go在你的电脑上的安装位置;
  • $GOARCH:表示目标机器的处理器架构,它的值可以是386、amd64或arm;
  • $GOOS:表示目标机器的操作系统,它的值可以是darwin、freebsd、linux或windows;
  • $GOBIN:表示编译器和链接器的安装位置,默认是$GOROOT/bin
  • $GOARM:专门针对基于arm架构的处理器,它的值可以是5或6,默认为6;
  • $GOMAXPROCS:用于设置应用程序可使用的处理器个数与核数;
  • $GOPATH:默认采用和$GOROOT一样的值,你必须修改为其它路径,它包含多个包含Go语言源码文件、包文件和可执行文件的路径,而这些路径下又必须分别包含三个规定的目录:src、pkg和bin,分别用于存放源码文件、包文件和可执行文件。

为了区分本地机器和目标机器,你可以使用$GOHOSTOS$GOHOSTARCH设置目标机器的参数,这两个变量只有在进行交叉编译的时候才会用到,如果你不进行显示设置,他们的值会和本地机器($GOOS$GOARCH)一样。

四、GO语言安装

在Go官网上,有详细的安装指南和下载地址:https://golang.org/doc/install。

安装完成后,需要设置一些环境变量($HOME/.bashrc):

Go语言的安装路径:

export GOROOT=$HOME/go

确保Go相关文件在系统任何地方都能被调用:

export PATH=$PATH:$GOROOT/bin

设置工作目录:

export GOPATH=$HOME/Application/go

工作目录可以有多个,使用分号隔开。

配置完成后,需要使用source .bashrc命令使这些环境变量生效。然后在终端输入go version验证是否可以使用go,使用go env来检查环境变量设置是否正确。

五、Sublime安装与使用:

  • 到官网下载最新版本:http://www.sublimetext.com/
  • 选择Tools->Install Package Control...,安装Package Control工具;https://packagecontrol.io/installation#ST3
  • 重启Sublime,在Preferences下就出现了Package Control选项。
  • 选择该选项,输入install,会出现Install Package,选中回车会出现插件安装包的对话框。
  • 这里输入gosublime和go build,安装这两个插件即可。
  • 进入Install Package,安装ctags插件,把Mouse Bindings-Default内容复制到Mouse Bindings-User中,重启Sublime,即可支持跨文件之间的函数跳转。

第二部分:语言的核心结构与技术

六、基本结构和基本数据类型

6.1 文件名、关键字与标识符

Go源文件以.go为后缀名存储。Go 语言也是区分大小写的,有效的标识符必须以字符(可以使用任何UTF-8编码的字符或_)开头,然后紧跟着0个或多个字符或Unicode数字。

_本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。

  • 程序一般由关键字、常量、变量、运算符、类型和函数组成。
  • 程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}。
  • 程序中可能会使用到这些标点符号:'.'、','、';'、':'和'…'。
  • 程序的代码通过语句来实现结构化,每个语句不需要像 C 家族中的其它语言一样以分号‘;’结尾,因为这些工作都将由 Go 编译器自动完成。
  • 如果你打算将多个语句写在同一行,它们则必须使用‘;’人为区分,但在实际开发中我们并不鼓励这种做法。

6.2 Go程序的基本结构和要素

  1. 包的概念

每个程序都由包(通常简称为pkg)的概念组成,包是结构化代码的一种方式,可以使用自身的包或者从其它包中导入内容。每个Go文件都属于且仅属于一个包,一个包可以由许多以.go为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。你必须在源文件中非注释的第一行指明这个文件属于哪个包,package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。需要注意的是:所有的包名都应该使用小写字母。

如果包名不是以‘.’或‘/’开头,如 "fmt" 或者 "container/list",则 Go 会在全局文件进行查找;如果包名以‘./’开头,则 Go 会在相对目录中查找;如果包名以‘/’ 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。

可以在导入包的时候,给包取一个别名:

import fm "fmt" // alias fm

  1. 可见性规则

当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public),遵循Pascal命名法;标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private ),遵循骆驼命名法。

导出的标识符,采用包名.标识符的形式进行访问。因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如pack1.Thingpack2.Thing

  1. 函数

这是定义一个函数的格式如下:

func functionName(parameter_list) (return_value_list) {
    // 函数体
}

main函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有init()函数则会先执行该函数)。main函数既没有参数,也没有返回类型(与 C 家族中的其它语言恰好相反)。

我们可以在init()函数中使用runtime.GOOS判断操作系统类型,从而做不同的程序准备,比如:

 var prompt = "Enter a digit, e.g. 3 "+ "or %s to quit."

 func init() {
     if runtime.GOOS == "windows" {
         prompt = fmt.Sprintf(prompt, "Ctrl+Z, Enter")        
     } else { //Unix-like
         prompt = fmt.Sprintf(prompt, "Ctrl+D")
     }
 }

Go语言函数使用comma, ok模式的返回值类型:返回某个值以及 true 表示成功;返回零值(或 nil)和 false 表示失败。当不使用 true 或 false 的时候,也可以使用一个 error 类型的变量来代替作为第二个返回值:成功执行的话,error 的值为 nil,否则就会包含相应的错误信息。习惯用法:

value, err := pack1.Function1(param1)
if err != nil {
    fmt.Printf("An error occured in pack1.Function1 with parameter %v", param1)
    return err
}
// 未发生错误,继续执行:

// 或者使用os包的Exit()函数
if err != nil {
    fmt.Printf("Program stopping with error %v", err)
    os.Exit(1)
}
  1. 注释

单行注释是最常见的注释形式,你可以在任何地方使用以//开头的单行注释。多行注释也叫块注释,均已以/*开头,并以*/结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。

  1. 类型

类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、slice、map、channel;只描述类型的行为的,如:interface。

结构化的类型没有真正的值,它使用 nil 作为默认值。值得注意的是,Go 语言中不存在类型继承。

函数也可以是一个确定的类型,就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后,例如:

func FunctionName (a typea, b typeb) typeFunc

使用 type 关键字可以定义你自己的类型(比如结构体),也可以定义一个已经存在的类型的别名。

每个值都必须在经过编译后属于某个类型(编译器必须能够推断出所有值的类型),因为 Go 语言是一种静态类型语言。

Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样(类型在这里的作用可以看作是一种函数):

valueOfTypeB = typeB(valueOfTypeA)

6.3 常量与变量

常量使用关键字 const 定义,用于存储不会改变的数据。存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。常量的定义格式:

const identifier1, identifier2, ... [type] = value1, value2, ...

常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。常量也可以用于枚举:

const (
    Unknown = 0
    Female = 1
    Male = 2
)

const (
    Sunday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

变量的命名规则遵循骆驼命名法,但如果你的全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写。声明变量的一般形式是使用 var 关键字:

var identifier type。

当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。

如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明;也不可以在定义变量之前使用它;声明了一个局部变量却没有在相同的代码块中使用它,会得到编译错误,单纯地给 a 赋值也是不够的,这个值必须被使用;但是全局变量是允许声明但不使用。

如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a。

空白标识符 _ 也被用于抛弃值,如值 5 在:, b = 5, 7 中被抛弃。 实际上是一个只写变量,你不能得到它的值。

变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。每一个源文件都可以包含一个或多个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。

6.4 运算符优先级

下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:

优先级 运算符
7 ^, !
6 *, /, %, <<, >>, &, &, ^
5 +, -, |, ^
4 ==, !=, <, <=, >=, >
3 <-
2 &&
1 ||

6.4 指针

一个指针变量可以指向任何一个值的内存地址,它指向那个值的内存地址。可以声明指针指向任何类型的值来表明它的原始性或结构性;你可以在指针类型前面加上 号(前缀)来获取指针所指向的内容,这里的 号是一个类型更改器。使用一个指针引用一个值被称为间接引用。当一个指针被定义后没有分配到任何变量时,它的值为 nil。你不能得到一个文字或常量的地址,例如:

const i = 5
ptr := &i //error: cannot take the address of i
ptr2 := &10 //error: cannot take the address of 10

6.5 常用的几个包

  • strings包定义了对字符串的常用操作;
  • strconv包定义了字符串与其他类型之间的转换;
  • time包定义了时间的获取与格式化等操作。

可以通过如下命令来查看包或者包内函数的详细使用方法:

go doc package function

七、控制结构

7.1 if-else结构

关键字 if 和 else 之后的左大括号 { 必须和关键字在同一行,如果你使用了 else-if 结构,则前段代码块的右大括号 } 必须和 else-if 关键字在同一行。

if condition1 {
    // do something    
} else if condition2 {
    // do something else    
}else {
    // catch-all or default
}

if 可以包含一个初始化语句(如:给一个变量赋值)。这种写法具有固定的格式(在初始化语句后方必须加上分号):

if initialization; condition {
    // do something
}

使用简短方式 := 声明的变量的作用域只存在于 if 结构中(在 if 结构的大括号之间,如果使用 if-else 结构则在 else 代码块中变量也会存在)。如果变量在 if 结构之前就已经存在,那么在 if 结构中,该变量原来的值会被隐藏。

7.2 switch结构

Go 语言中的 switch 结构使用上更加灵活。它接受任意形式的表达式:

switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。前花括号 { 必须和 switch 关键字在同一行。您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3。每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。

一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说您不需要特别使用 break 语句来表示结束。如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough 关键字来达到目的。

您同样可以使用 return 语句来提前结束代码块的执行。当您在 switch 语句块中使用 return 语句,并且您的函数是有返回值的,您还需要在 switch 之后添加相应的 return 语句以确保函数始终会返回。

可选的 default 分支可以出现在任何顺序,但最好将它放在最后。它的作用类似与 if-else 语句中的 else,表示不符合任何已给出条件时,执行相关语句。

switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true),然后在每个 case 分支中进行测试不同的条件。当任一分支的测试结果为 true 时,该分支的代码会被执行。这看起来非常像链式的 if-else 语句,但是在测试条件非常多的情况下,提供了可读性更好的书写方式。

switch {
    case condition1:
        ...
    case condition2:
        ...
    default:
        ...
}

switch 语句的第三种形式是包含一个初始化语句:

switch initialization; {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

7.3 for结构

基本形式:

for 初始化语句; 条件语句; 修饰语句 {
    // 循环体
}

没有头部的条件判断迭代,格式为:

for 条件语句 {
    // 循环体
}

无线循环:

for {
}
for ;; {
}
for true {
}

for-range结构,词结构类似于foreach语句,一般形式为:

for pos, char := rang str {
}

7.4 break、continue与goto

  • break语句用于打断当前for循环或跳出switch语句,开始执行后面的语句;在多重循环中,可以使用标号label标出想break的循环(注意是break,不是goto);
  • continue语句用于跳过当前循环的剩余语句,然后继续执行下一轮循环,continue语句会触发for增量语句的执行;在多重循环中,可以使用标号label标出想continue的循环;
  • goto语句可以无条件地将控制转移到被标记的语句,它常与条件语句配合,实现条件转移、构成循环或跳出循环等功能;在结构化程序设计中,不主张使用goto,以免造成程序流程混乱,加大理解和调试的困难。

标签是以冒号(:)结尾的单词。标签和 goto 语句之间不能出现定义新变量的语句,否则会导致编译失败。

八、函数

好的程序是非常注意DRY原则的,即不要重复你自己(Don't Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次。

8.1 函数的定与使用

return 语句可以带有零个或多个参数,return 语句也可以用来结束 for 死循环,或者结束一个协程(goroutine)。

Go 里面有三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者lambda函数
  • 方法(Methods)

除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。Go语言里面函数重载是不被允许的。

函数被调用的基本格式如下:

pack1.Function(arg1, arg2, …, argn)

函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的,例如:假设 f1 需要 3 个参数 f1(a, b, c int),同时 f2 返回 3 个参数 f2(a, b int) (int, int, int),就可以这样调用 f1:f1(f2(a, b))。

如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:

func flushICache(begin, end uintptr) // implemented externally

函数也可以以申明的方式被使用,作为一个函数类型,就像:

type binOp func(int, int) int

函数值(functions value)之间可以相互比较:如果它们引用的是相同的函数或者都是 nil 的话,则认为它们是相同的函数。函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数来破除这个限制。

目前 Go 没有泛型(generic)的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口(interface),特别是空接口与类型选择(type switch)与/或者通过使用反射(reflection)来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强。

在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。

当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int)。命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来。即使函数使用了命名返回值,你依旧可以无视它而返回明确的值。

尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。

空白符'_'用来匹配一些不需要的值,然后丢弃掉。

8.2 变长参数

如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数

func myFunc(a, b, arg ...int) {}

这个函数接受一个类似某个类型的 slice 的参数,该参数可以通过 for 循环结构迭代。比如:

func Greeting(prefix string, who ...string)
Greeting("Hello:", "Joe", "Anna", "Eileen")

则上面的变量who的值为[]string{"Joe", "Anna", "Eileen"}。一个接受变长参数的函数可以将这个参数作为其它函数的参数进行传递,变长参数可以作为对应类型的 slice 进行二次传递。

function F1(s  string) {
    F2(s )
    F3(s)
}

func F2(s  string) { }
func F3(s []string) { }

但是如果变长参数的类型并不是都相同的呢?我们可以定义结构体来传递,也可以使用**空接口interface{}**来实现,这样就可以接受任何类型的参数。空接口方案不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。一般而言我们会使用一个 for-range 循环以及 switch 结构对每个参数的类型进行判断:

 func typecheck(..,..,values  interface{}) {
     for _, value := range values {
         switch v := value.(type) {
             case int: 
             case float: 
             case string: 
             case bool: 
             default: 
         }
     }
 }

8.3 defer和追踪

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return 语句同样可以包含一些操作,而不是单纯地返回某个值)。

当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)。合理使用 defer 语句能够使得代码更加简洁。关键字 defer 允许我们进行一些函数执行完成后的收尾工作,例如:

  • 关闭文件流:defer file.close()
  • 解锁一个加锁资源:defer mu.Unlock()
  • 打印最终报告:defer printFooter()
  • 关闭数据库链接:defer disconnectFromDB()

一个基础但十分实用的实现代码执行追踪的方案就是在进入和离开某个函数打印相关的消息,即可以提炼为下面两个函数:

func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

也可以使用 defer 语句在函数返回时,记录函数的参数与返回值。

8.4 内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数:

名称 说明
close 用于管道通信
len、cap len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
new、make new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针.它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作.new() 是一个函数,不要忘记它的括号。
copy、append 用于复制和连接切片
panic、recover 两者均用于错误处理机制
print、println 底层打印函数,在部署环境中建议使用 fmt 包
complex、real imag 用于创建和操作复数

Go 语言中也可以使用相互调用的递归函数:多个函数之间相互调用形成闭环。

函数也可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。

8.5 匿名函数与闭包

当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }。这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fplus(3,4)。当然,您也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)

表示参数列表的第一对括号必须紧挨着关键字 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。

type binOp func(a, b int) int
func run(op binOp, req *Request) {  }

关键字defer经常配合匿名函数使用,它可以用于改变函数的命名返回值。匿名函数还可以配合 go 关键字来作为 goroutine 使用。

匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁。闭包经常被用作包装函数:它们会预先定义好 1 个或多个参数以用于包装。另一个不错的应用就是使用闭包来完成更加简洁的错误检查。

使用闭包调试

当您在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtime 或 log 包中的特殊函数来实现这样的功能。包 runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候实现一个 where() 闭包函数来打印函数执行的位置:

where := func() {
    _, file, line, _ := runtime.Caller(1)
    log.Printf("%s:%d", file, line)
}
where()
// some code
where()
// some more code
where()

或者更加简洁版本的where函数:

var where = log.Print
func func1() {
where()
... some code
where()
... some code
where()
}

同时,需要设置一下log包的flag参数,默认输出相关信息:

log.SetFlags(log.Llongfile)
log.Print("")

8.6计算函数的执行时间

有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 time 包中的 Now() 和 Sub 函数:

start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)

8.7 通过内存缓存来提升性能

当在进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存。

内存缓存的优势显而易见,而且您还可以将它应用到其它类型的计算中,例如使用 map 而不是数组或切片。

九、数组与切片

数组是具有相同 唯一类型 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以[5]int和[10]int是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的。

数组元素可以通过 索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。声明的格式是:

var identifier [len]type

注意事项:如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型。当使用值时我们必须先做一个类型判断。

Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建:var arr1 = new([5]int)

package main
import "fmt"
func f(a [3]int) { fmt.Println(a) }
func fp(a *[3]int) { fmt.Println(a) }

func main() {
    var ar [3]int
    f(ar)     // passes a copy of ar
    fp(&ar) // passes a pointer to ar
}

把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:

  • 传递数组的指针;
  • 使用数组的切片。

切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。切片可以通过len()获取长度,通过cap()获取容量,并且:0 <= len(s) <= cap(s)

多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。

优点:因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。

切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片 长度以及切片容量。

注意:绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!

当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片 同时创建好相关数组:

var slice1 []type = make([]type, len, [cap])

下面两种方法可以生成相同的切片:

make([]int, 50, 100)
new([100]int)[0:50]

new()make()的区别:

  • new(T)为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体,它相当于 &T{}。
  • make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel。

换言之,new 函数分配内存,make 函数初始化。

bytes 包和字符串包十分类似,而且它还包含一个十分有用的类型 Buffer:

import "bytes"

type Buffer struct {
    ...
}

Buffer 可以这样定义:var buffer bytes.Buffer

或者使用 new 获得一个指针:var r *bytes.Buffer = new(bytes.Buffer)

或者通过函数:func NewBuffer(buf []byte) *Buffer,创建一个 Buffer 对象并且用 buf 初始化好;NewBuffer 最好用在从 buf 读取的时候使用。

for-range结构可以用于数组和切片:

for ix, value := range slice1 {
    ...
}

如果你只需要索引,你可以忽略第二个变量:

for ix := range slice1 {
    ...
}

比如二维切片行、列和矩阵值:

for row := range screen {
    for column := range screen[row] {
        screen[row][column] = 1
    }
}

改变切片长度的过程称之为切片重组 reslicing,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即长度)。切片可以反复扩展直到占据整个相关数组。

func copy(dst, src []T) int 方法将类型为 T 的切片从源地址 src 拷贝到目标地址 dst,覆盖 dst 的相关元素,并且返回拷贝的元素个数。源地址和目标地址可能会有重叠。拷贝个数是 src 和 dst 的长度最小值。如果 src 是字符串那么元素类型就是 byte。如果你还想继续使用 src,在拷贝结束后执行 src = dst。

func append(s[]T, x ...T) []T 方法将 0 个或多个具有相同类型 s 的元素追加到切片后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了。append 方法总是返回成功,除非系统内存耗尽了。

如果你想将切片 y 追加到切片 x 后面,只要将第二个参数扩展成一个列表即可:x = append(x, y...)

假设 s 是一个字符串(本质上是一个字节数组),那么就可以直接通过 c := []byte(s) 来获取一个字节的切片 c。另外,您还可以通过 copy 函数来达到相同的目的:copy(dst []byte, src string)。

Go 语言中的字符串是不可变的,也就是说 str[index] 这样的表达式是不可以被放在等号左侧的。如果尝试运行 str[i] = 'D' 会得到错误:cannot assign to str[i]。因此,您必须先将字符串转换成字节数组,然后再通过修改数组中的元素值来达到修改字符串的目的,最后将字节数组转换回字符串格式。

s := "hello"
c := []byte(s)
c[0] = c
s2 := string(c) // s2 == "cello"

append 非常有用,它能够用于各种方面的操作:

  1. 将切片 b 的元素追加到切片 a 之后:a = append(a, b...)
  2. 删除位于索引 i 的元素:a = append(a[:i], a[i+1:]...)
  3. 切除切片 a 中从索引 i 至 j 位置的元素:a = append(a[:i], a[j:]...)
  4. 为切片 a 扩展 j 个元素长度:a = append(a, make([]T, j)...)
  5. 在索引 i 的位置插入元素 x:a = append(a[:i], append([]T{x}, a[i:]...)...)
  6. 在索引 i 的位置插入长度为 j 的新切片:a = append(a[:i], append(make([]T, j), a[i:]...)...)
  7. 在索引 i 的位置插入切片 b 的所有元素:a = append(a[:i], append(b, a[i:]...)...)
  8. 取出位于切片 a 最末尾的元素 x:x, a = a[len(a)-1], a[:len(a)-1]
  9. 将元素 x 追加到切片 a:a = append(a, x)

因此,您可以使用切片和 append 操作来表示任意可变长度的序列。从数学的角度来看,切片相当于向量,如果需要的话可以定义一个向量作为切片的别名来进行操作。

切片的底层指向一个数组,该数组的实际容量可能要大于切片所定义的容量。只有在没有任何切片指向的时候,底层的数组内存才会被释放,这种特性有时会导致程序占用多余的内存。因此,推荐根据实际使用的数据,创建一个新的切片只保存我们需要的数据,从而保障底层数组的大内存能够被及时的释放。

十、集合(Map)

map是一种特殊的数据结构:一种元素对(pair)的无序集合,pair的一个元素是key,对应的另外一个元素是value,所以这个结构也称为关联数组或字典。map是引用类型,内存用make方法来分配。声明方式如下:

var map1 map[keytype]valuetype

声明时不需要知道map的长度,其可以动态增长的,未初始化的map值是nilkey是可以任意用==或者!=操作符比较的类型,比如stringintfloat等,所以数组、切片和结构体不能作为key,但是指针和接口类型可以。如果要用结构体作为key可以提供Key()Hash()方法,这样可以通过结构体的域计算出唯一的数字或者字符串的key

常用的len(map1)方法可以获得map中的pair数目,这个数目是可以伸缩的,因为map-pairs在运行时可以动态添加和删除。

map初始化:

var map1[keytype]valuetype = make(map[keytype]valuetype)
map1 := make(map[keytype]valuetype)

下面的语句可以获取到对应key1的元素是否存在:

val1, isPresent := map1[key1]

if _, ok := map1[key1]; ok {
    ...
}

删除map中的元素,直接使用delete即可,若元素不存在,不会产生错误:

delete(map1, key1)

也可以使用for循环轮询````map```:

for key, value := range map1 {
    ...
}

// 或者只获取value值
for _, value := range map1 {
    ...
}

// 或者只获取key值
for key := range map1 {
    ...
}

map类型的切片:

package main
import "fmt"

func main() {
    // Version A:
    items := make([]map[int]int, 5)
    for i:= range items {
        items[i] = make(map[int]int, 1)
        items[i][1] = 2
    }
    fmt.Printf("Version A: Value of items: %v\n", items)

    // Version B: NOT GOOD!
    items2 := make([]map[int]int, 5)
    for _, item := range items2 {
        item = make(map[int]int, 1) // item is only a copy of the slice element.
        item[1] = 2 // This 'item' will be lost on the next iteration.
    }
    fmt.Printf("Version B: Value of items: %v\n", items2)
}

输出结果为:

Version A: Value of items: [map[1:2] map[1:2] map[1:2] map[1:2] map[1:2]]
Version B: Value of items: [map[] map[] map[] map[] map[]]

应当像 A 版本那样通过索引使用切片的 map 元素。在 B 版本中获得的项只是 map 值的一个拷贝而已,所以真正的 map 元素没有得到初始化。

map 默认是无序的,不管是按照 key 还是按照 value 默认都不排序。如果你想为 map 排序,需要将 key(或者 value)拷贝到一个切片,再对切片排序(使用 sort 包),然后可以使用切片的 for-range 方法打印出所有的 key 和 value。

十一、包(package)

11.1 标准库概述

fmtos等这样具有常用功能的内置包在Go语言中有150个以上,它们被称为标准库,大部分(一些底层的除外)内置于Go本身,完整列表可以在Go Walker查看。

  • unsafe: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。
  • syscall-os-os/exec:
    • os: 提供给我们一个平台无关性的操作系统功能接口,采用类Unix设计,隐藏了不同操作系统间差异,让不同的文件系统和操作系统对象表现一致。
    • os/exec: 提供我们运行外部操作系统命令和程序的方式。
    • syscall: 底层的外部包,提供了操作系统底层调用的基本接口。
  • archive/tar 和 /zip-compress:压缩(解压缩)文件功能。
  • fmt-io-bufio-path/filepath-flag:
    • fmt: 提供了格式化输入输出功能。
    • io: 提供了基本输入输出功能,大多数是围绕系统功能的封装。
    • bufio: 缓冲输入输出功能的封装。
    • path/filepath: 用来操作在当前系统中的目标文件名路径。
    • flag: 对命令行参数的操作。
  • strings-strconv-unicode-regexp-bytes:
    • strings: 提供对字符串的操作。
    • strconv: 提供将字符串转换为基础类型的功能。
    • unicode: 为 unicode 型的字符串提供特殊的功能。
    • regexp: 正则表达式功能。
    • bytes: 提供对字符型分片的操作。
    • index/suffixarray: 子字符串快速查询。
  • math-math/cmath-math/big-math/rand-sort:
    • math: 基本的数学函数。
    • math/cmath: 对复数的操作。
    • math/rand: 伪随机数生成。
    • sort: 为数组排序和自定义集合。
    • math/big: 大数的实现和计算。   
  • container-/list-ring-heap: 实现对集合的操作。
    • list: 双链表。
    • ring: 环形链表。
  • time-log:
    • time: 日期和时间的基本操作。
    • log: 记录程序运行时产生的日志,我们将在后面的章节使用它。
  • encoding/json-encoding/xml-text/template:
    • encoding/json: 读取并解码和写入并编码 JSON 数据。
    • encoding/xml:简单的 XML1.0 解析器,有关 JSON 和 XML 的实例请查阅第 12.9/10 章节。
    • text/template:生成像 HTML 一样的数据与文本混合的数据驱动模板(参见第 15.7 节)。
  • net-net/http-html:(参见第 15 章)
    • net: 网络数据的基本操作。
    • http: 提供了一个可扩展的 HTTP 服务器和基础客户端,解析 HTTP 请求和回复。
    • html: HTML5 解析器。
  • runtime: Go 程序运行时的交互操作,例如垃圾回收和协程创建。
  • reflect: 实现通过程序运行时反射,让程序操作任意类型的变量。

包是 Go 语言中代码组织和代码编译的主要方式。

  1. 锁和sync包

sync.Mutex是一个互斥锁,它的作用是守护在临界区入口来确保同一时间只能有一个线程进入临界区。

import "sync"

type Info struct {
    mu sync.Mutex
    str string
    // other fields
}

func Update(info *Info) {
    info.mu.Lock()
    info.str = ...
    info.mu.Unlock()
}
  1. 精密计算和big包

若要进行高精度计算,可以使用big包:bit.Int类型表示大整数,由bit.NewInt(n)构造;big.Rat类型表示大有理数,通过big.NewRat(N, D)来构造。big包缺点是需要更大的内存和跟多的处理开销。

十二、结构(struct)与方法(method)

12.1 结构(struct)

Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。结构体也是值类型,因此可以通过 new 函数来创建。

组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。结构体定义的一般方式如下:

type identifier struct {
    field1 type1
    field2 type2
    ...
}

type T struct {a, b int}也是合法的语法,它更适用于简单的结构体。

使用new函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T)。声明var t T也会给t分配内存,并零值化内存,但是这个时候t是类型T

可以使用点号符给字段赋值:structname.fieldname = value;也可以使用点号符获取结构体字段的值:structname.fieldname。在Go语言中这叫选择器(selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的**选择器符(selector-notation)***来引用结构体的字段。初始化一个结构体实例:

ms := &struct1{10, 15.5, "Chris"}
// 此时ms的类型是 *struct1

var ms struct1
ms = struct1{10, 15.5, "Chris"}

表达式new(Type)&Type{}是等价的。

结构体类型和字段的命名遵循可见性规则,一个导出的结构体类型中有些字段是导出的,另一些不是,这是可能的。

如果想知道结构体类型T的一个实例占用了多少内存,可以使用:size := unsafe.Sizeof(T{})

newmake看起来没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。

  • new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体,它相当于 &T{};
  • make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel。

换言之,new 函数分配内存,make 函数初始化。

12.2 方法(method)

Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。

一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。

定义方法的一般格式如下:

func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

在方法名之前,func 关键字之后的括号中指定 receiver。如果方法不需要使用 recv 的值,可以用 _ 替换它:

func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }

因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的:

func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix

recv 就像是面向对象语言中的 this 或 self,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 this 或 self 作为 receiver 的名字。

类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float 或类似这些的类型上定义方法。试图在 int 类型上定义方法会得到一个编译错误:

cannot define new methods on non-local type int

但是有一个间接的方式:可以先定义该类型(比如:int 或 float)的别名类型,然后再为别名类型定义方法。或者将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。

指针方法和值方法都可以在指针或非指针上被调用。

当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型继承了这些方法:将父类型放在子类型中来实现亚型

内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法,

可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。

结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。

主要有两种方法来实现在类型中嵌入功能:

  • A:聚合(或组合):包含一个所需功能类型的具名字段。
  • B:内嵌:内嵌(匿名地)所需功能类型。

12.3 函数和方法的区别

  • 函数将变量作为参数:Function1(recv)
  • 方法在变量上被调用:recv.Method1()

接收者必须有一个显式的名字,这个名字必须在方法中被使用。receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。

在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫 组件编程(Component Programming)

十三、接口(interface)与反射(reflection)

Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 说明 对象的行为:接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。通过如下格式定义接口:

type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

Namer是接口类型。

按照约定,只包含一个方法的)接口的名字由方法名加 [e]r 后缀组成,例如 Printer、Reader、Writer、Logger、Converter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头。Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。

一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值:

v := varI.(T) // unchecked type assertion

varI 必须是一个接口变量,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of varI) on left)。更安全的方式是使用以下形式来进行类型断言:

if v, ok := varI.(T); ok {  // checked type assertion
    Process(v)
    return
}
// varI is not of type T

if _, ok := varI.(T); ok {
    // ...
}

我们也可以使用switch来进行类型匹配:

switch t := areaIntf.(type) {
case *Square:
    fmt.Printf("Type Square %T with value %v\n", t, t)
case *Circle:
    fmt.Printf("Type Circle %T with value %v\n", t, t)
case nil:
    fmt.Printf("nil value: nothing to check?\n")
default:
    fmt.Printf("Unexpected type %T\n", t)
}

下面的代码片段展示了一个类型分类函数,它有一个可变长度参数,可以是任意类型的数组,它会根据数组元素的实际类型执行不同的动作:

func classifier(items ...interface{}) {
    for i, x := range items {
        switch x.(type) {
        case bool:
            fmt.Printf("Param #%d is a bool\n", i)
        case float64:
            fmt.Printf("Param #%d is a float64\n", i)
        case int, int64:
            fmt.Printf("Param #%d is a int\n", i)
        case nil:
            fmt.Printf("Param #%d is a nil\n", i)
        case string:
            fmt.Printf("Param #%d is a string\n", i)
        default:
            fmt.Printf("Param #%d is unknown\n", i)
        }
    }
}

可以这样调用此方法:classifier(13, -14.3, "BELGIUM", complex(1, 2), nil, false)

定 v 是一个值,然后我们想测试它是否实现了 Stringer 接口,可以这样做:

type Stringer interface {
    String() string
}

if sv, ok := v.(Stringer); ok {
    fmt.Printf("v implements String(): %s\n", sv.String()) // note: sv, not v
}

编写参数是接口变量的函数,这使得它们更具有一般性。使用接口使代码更具有普适性。

第三部分:Go高级编程

十四、读写数据

十五、错误处理与测试

十六、协程(goroutine)与通道(channel)

十七、网络、模板与网页应用

第四部分:实际应用

十八、常见的陷阱与错误

十九、模式

二十、实用代码片段

##################################

常用命令与工具

go run go fmt go doc go build go install go test

标准库包和如何创建自己的包

与其他语言混合编程

C C++

Go
1
https://gitee.com/mitslyj/Golang.git
git@gitee.com:mitslyj/Golang.git
mitslyj
Golang
Golang
master

搜索帮助