Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

第一部分:学习Go语言

第一章:起源于发展

起源与发展

Go 语言起源 2007 年,并于 2009 年正式对外发布。

时间轴:

  • 2007 年 9 月 21 日:雏形设计
  • 2009 年 11 月 10日:首次公开发布
  • 2010 年 1 月 8 日:当选 2009 年年度语言
  • 2010 年 5 月:谷歌投入使用
  • 2011 年 5 月 5 日:Google App Engine 支持 Go 语言

主要特性

发展目标:将静态语言的安全性和高效性与动态语言的易开发性进行有机结合。是对于网络通信、并发和并行编程的极佳支持,从而更好地利用大量的分布式和多核的计算机。

Go 语言是一门类型安全和内存安全的编程语言。虽然 Go 语言中仍有指针的存在,但并不允许进行指针运算。

重要的特性:

  • 构建速度(编译和链接到机器代码的速度)快。
  • 使用包模式的依赖管理更加的清晰。
  • 执行速度快。
  • 没有类和继承的概念,通过接口(interface)的概念来实现多态性。
  • 使用静态类型,所以它是类型安全的一门语言。
  • 强类型语言,隐式的类型转换是不被允许。
  • 动态语言的特性(通过关键字 var)。
  • 支持交叉编译。

LALR 是 Go 语言的语法标准。

Go语言的用途:

  • 应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。
  • 实现所谓的复杂事件处理(CEP)。
  • Go 语言不适合用来开发对实时性要求很高的软件。

通过 recoverpanic 来替代异常机制

第二章:安装与运行环境

架构

2个版本的编译器:Go 原生编译器 gc 和非原生编译器 gccgo,这两款编译器都是在类 Unix 系统下工作 。

Go从1.5版本开始已经实现自举。

Go 语言源文件的扩展名很显然就是 .go

创建目录时,文件夹名称永远不应该包含空格,而应该使用下划线 “_” 或者其它一般符号代替。

环境变量

  • $GOROOT 表示 Go 在你的电脑上的安装位置
  • $GOARCH 表示目标机器的处理器架构
  • $GOBIN 表示编译器和链接器的安装位置
  • $GOPATH三个规定的目录:srcpkgbin,这三个目录分别用于存放源码文件、包文件和可执行文件。

Go 编译器支持交叉编译,可以使用 $GOHOSTOS$GOHOSTARCH 设置本地机器的操作系统名称和编译体系结构。

安装目录

  • /bin:包含可执行文件
  • /doc:包含示例程序,代码工具,本地文档等
  • /lib:包含文档模版
  • /src:包含源代码构建脚本和标准库的包的完整源代码

Go运行时(runtime)

代码仍旧运行在 Go 的 runtime当中。似 Java 和 .NET 语言所用到的虚拟机,它负责管理包括内存分配、垃圾回收)、栈处理、goroutine、channel、切片(slice)、map 和反射(reflection)等等。

垃圾回收器Go 拥有简单却高效的标记-清除回收器。

第三章:编辑与调试

编辑器与调试器

编辑器:**Sublime TextLiteIDEGoClipse**

调试器:可用的调试器是 gdb。

基本调试:

  1. 在合适的位置使用打印语句输出相关变量的值。
  2. fmt.Printf 中使用下面的说明符
    • %+v 打印包括字段在内的实例的完整信息
    • %#v 打印包括字段和限定类型名称在内的实例的完整信息。
    • %T 打印某个类型的完整说明

构建

格式化工具 gofmt 并保存格式化后的源文件。

构建应用程序:

  • go build 编译自身包和依赖包。
  • go install 编译并安装自身包和依赖包。

格式化代码

go fmtgofmt)。这个工具可以将你的源代码格式化成符合官方统一标准的风格,属于语法风格层面上的小型重构。

gofmt –w program.go 会格式化该源文件的代码然后将格式化后的代码覆盖原始内容。

gofmt -w *.go 会格式化并重写所有 Go 源文件。

gofmt map1 会格式化并重写 map1 目录及其子目录下的所有 Go 源文件。。

gofmt 也可以通过在参数 -r 后面加入用双引号括起来的替换规则实现代码的简单重构,规则的格式:<原始内容> -> <替换内容>

1
gofmt -r '(a) -> a' -w *.go

生成代码文档

go doc 工具会从 Go 程序和包文件中提取顶级声明的首行注释以及每个对象的相关注释,并生成相关文档。

一般用法

  • go doc package获取包的文档注释
  • go doc package/subpackage 获取子包的文档注释
  • go doc package function 获取某个函数在某个包中的文档注释

第四章:基本结构和基本数据类型

文件名、关键字与标识符

文件名均由小写字母组成,如 scanner.go 。如果文件名由多个部分组成,则使用下划线 _ 对它们进行分隔,scanner_test.go

有效的标识符必须以字母(可以使用任何 UTF-8 编码的字符或 _)开头。

_ 本身就是一个特殊的标识符,被称为空白标识符。

程序一般由关键字、常量、变量、运算符、类型和函数组成。

程序的基本结构和要素

包、导入与可见性

包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。

每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。

package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。

所有的包名都应该使用小写字母。

属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。

每一段代码只会被编译一次

导入包的方式

1
2
3
4
5
6
7
8
9
10
11
import "fmt"
import "os"

import "fmt";import "os"

import (
"fmt"
"os"
)

import ("fmt";"os")

可见性规则

当标识符以一个大写字母开头,就可以被外部包的代码所使用,这被称为导出;标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用。

可以像面向对象语言那样使用点标记来调用:pack1.Thing

函数

1
func functionname()

main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。

函数简短

1
func Sum(a,b int) int {return a+b}

只有当某个函数需要被外部包调用的时候才使用大写字母开头

注释

注释不会被编译,但可以通过 godoc 来使用。

// 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾

每一个包应该有相关注释,在 package 语句之前的块注释将被默认认为是这个包的文档说明。

几乎所有全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释。如果这种注释(称为文档注释)出现在函数前面,例如函数 Abcd,则要以 "Abcd..." 作为开头。

1
2
3
4
5
// enterOrbit causes Superman to fly into low Earth orbit, a position
// that presents several possibilities for planet salvation.
func enterOrbit() error {
...
}

类型

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

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

函数也可以是一个确定的类型,就是以函数作为返回类型。

一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 () 将它们括起来,如:

1
func Functionname (a int,b int) (c int,d int)

type 关键字可以定义你自己的类型,你可能想要定义一个结构体,但是也可以定义一个已经存在的类型的别名,如:

1
type IZ int

并不是真正意义上的别名,使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。

多个类型定义:

1
2
3
4
5
type (
IZ int
FZ float64
STR string
)

程序的一般结构

  • 在完成包的 import 之后,开始对常量、变量和类型的定义或声明。

  • 如果存在 init 函数的话,则对该函数进行定义。

  • 如果当前包是 main 包,则定义 main 函数。

  • 然后定义其余的函数,首先是类型的方法,接着是按照 main 函数中先后调用的顺序来定义相关函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
)
const PI = 3.14
var h float32 = 4
type Yuan struct{
r float32
}
func init(){
}
func main(){
y :=&Yuan{9}
Fun(y.mianji() * h)
}
func (y Yuan) mianji() float32{
return PI * y.r * y.r
}
func Fun(v float32){
fmt.Printf("mianji%f",v)
}

类型转换

Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明。

类型 B 的值 = 类型 B(类型 A 的值)

1
2
a := 5.0
b := int(5.0)

常量

常量使用关键字 const 定义,用于存储不会改变的数据。

存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

1
const Pi = 3.14

常量的值必须是能够在编译时就能够确定的。

因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()。

1
2
const Ln2 = 0.693147180559945309417232121458\
176568075500134360255254120680009

反斜杠 \ 可以在常量表达式中作为多行的连接符使用。

常量并行赋值

1
const beef,two,c = "eat",2,"vag"

iota 可以被用作枚举值:

1
2
3
4
5
const (
a = iota
b = iota
c = iota
)

简单地讲,每遇到一次 const 关键字,iota 就重置为 0。.

变量

声明变量的一般形式是使用 var 关键字:var identifier type

1
2
3
4
5
6
7
8
9
var a int
var b bool
var str string

var (
a int
b bool
str string
)

变量的命名规则遵循骆驼命名法。

全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写。

变量可以编译期间就被赋值,赋值给变量使用运算符等号 =,可以在运行时对变量进行赋值操作。

1
2
3
4
5
var a int
var b bool

a = 15
b = true

声明与赋值(初始化)语句也可以组合起来。

1
2
var a int = 15
var b bool = false

自动类型推断

1
2
3
4
5
6
7
8
9
10
var a = 15
var b = false

var (
a = 15
b = false
str = "Go says hello to the world!"
numShips = 50
city string
)

在函数体内声明局部变量时,应使用简短声明语法 :=,例如:

1
a := 1

实例:

1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"fmt"
"runtime"
"os"
)
func main (){
goos := runtime.GOOS
fmt.Printf("%s\n",goos)
var path string = os.Getenv("PATH")
fmt.Printf("%s\n",path)
}

值类型和引用类型

int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值。

数组和结构体这些复合类型是值类型。

指针、切片、映射和通道是引用类型。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。

函数 fmt.SprintfPrintf 的作用是完全相同的,不过前者将格式化后的字符串以返回值的形式返回给调用者。

在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 就是不被允许的。

1
a, b, c := 5, 7, "abc"

交换两个变量的值,则可以简单地使用 a, b = b, a

init函数

变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。

每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。

用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。

1
2
3
4
5
6
package trans
import "math"
var Pi float64
func init(){
Pi = 4*math.Atan(1)
}
1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
"./trans"
)
var twoPi = 2*trams.Pi
func main(){
fmt.Printf(twoPi)
}

init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine。

1
2
3
4
func init() {
// setup preparations
go backend()
}

基本类型与运算符

bool类型

只有两个类型相同的值才可以进行比较,如果值的类型是接口,它们也必须都实现了相同的接口。

布尔型的常量和变量也可以通过和逻辑运算符(非 !、和 &&、或 ||)结合来产生另外一个布尔值。

非运算符用于取得和布尔值相反的结果。

&&两边的值都为 true 的时候,结果才是 true。

||两边的值都为 false 的时候,结果才是 false。

在格式化输出时,你可以使用 %t 来表示你要输出的值为布尔型。

数字类型

Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。

Go 也有基于架构的类型,例如:int、uint 和 uintptr。

这些类型的长度都是根据运行程序所在的操作系统类型所决定的:

  • intuint 在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)。
  • uintptr 的长度被设定为足够存放一个指针即可。

Go 语言中没有 float 类型。(Go语言中只有 float32 和 float64)没有double类型。

int 型是计算最快的一种类型。

float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。

尽可能地使用 float64,因为 math 包中所有有关数学运算的函数都会要求接收这个类型。

前缀 0 来表示 8 进制数(如:077),增加前缀 0x 来表示 16 进制数(如:0xFF),以及使用 e 来表示 10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)。

可以使用 a := uint64(0) 来同时完成类型转换和赋值操作,这样 a 的类型就是 uint64。

Go 中不允许不同类型之间的混合使用,但是对于常量的类型限制非常少,因此允许常量之间的混合使用:

1
2
3
4
5
6
7
8
package main
func main(){
var a int
var b int32
a = 15
b = a+a //编译错误
b = b+5 //5是常量,可以编译
}

格式化说明符

%d 用于格式化整数(%x%X 用于格式化 16 进制表示的数字),%g 用于格式化浮点型(%f 输出浮点数,%e 输出科学计数表示法),%0nd 用于规定输出长度为n的整数,其中开头的数字 0 是必须的。

%n.mg 用于表示数字 n 并精确到小数点后 m 位,除了使用 g 之外,还可以使用 e 或者 f,例如:使用格式化字符串 %5.2e 来输出 3.4 的结果为 3.40e+00

/ 对于整数运算而言,结果依旧为整数,例如:9 / 4 -> 2

取余运算符只能作用于整数:9 % 4 -> 1

对于整数和浮点数,你可以使用一元运算符 ++(递增)和 --(递减),但只能用于后缀。

++-- 的只能作为语句,而非表达式,因此 n = i++ 这种写法是无效的。

随机数

rand 包实现了伪随机数的生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"math/rand"
"time"
)
func main(){
for i:=0; i<5; i++{
a := rand.Int()
fmt.Printf("%d\n",r)
}
for i := 0; i < 5; i++ {
r := rand.Intn(8)
fmt.Printf("%d / ", r)
}
times := int64(time.Now().Nanosecond())
rand.Seed(times)
for i := 0; i < 10; i++ {
fmt.Printf("%2.2f / ", 100*rand.Float32())
}
}

函数 rand.Intn 返回介于 [0, n) 之间的伪随机数。

类型别名

type TZ int 中,TZ 就是 int 类型的新名称(用于表示程序中的时区),然后就可以使用 TZ 来操作 int 类型的数据。

新类型不会拥有原类型所附带的方法。

字符类型

byte 类型是 uint8 的别名。

Go 同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。

rune 也是 Go 当中的一个类型,并且是 int32 的别名。

1
var ch byte = 65var ch byte = '\x41'

var ch byte = 'A';字符使用单引号括起来。

Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则会加上 \U 前缀;前缀 \u 则总是紧跟着长度为 4 的 16 进制数,前缀 \U 紧跟着长度为 8 的 16 进制数。

1
2
3
4
5
6
7
8
9
10
11
12
var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3) // UTF-8 code point

65 - 946 - 1053236
A - β - r
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234
  • 判断是否为字母:unicode.IsLetter(ch)
  • 判断是否为数字:unicode.IsDigit(ch)
  • 判断是否为空白符号:unicode.IsSpace(ch)

utf8 拥有更多与 rune 类型相关的函数。

字符串

字符串是一种值类型,且值不可变,字符串是字节的定长数组。

Go 中的字符串是根据长度限定,而非特殊字符\0

函数 len() 来获取字符串所占的字节长度。

在循环中使用加号 + 拼接字符串并不是最高效的做法,更好的办法是使用函数 strings.Join()。

使用字节缓冲(bytes.Buffer)拼接更加给力。

strings和strconv包

Go 中使用 strings 包来完成对字符串的主要操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
strings.HasPrefix(s, prefix string) bool
// 判断字符串 s 是否以 prefix 开头

strings.HasSuffix(s, suffix string) bool
// 判断字符串 s 是否以 suffix 结尾

strings.Contains(s, substr string) bool
// 判断字符串 s 是否包含 substr

strings.Index(s, str string) int
// Index 返回字符串 str 在字符串 s 中的索引
strings.LastIndex(s, str string) int
// LastIndex 返回字符串 str 在字符串 s 中最后出现位置的索引
strings.IndexRune(s string, r rune) int
// 非 ASCII 编码的字符在父字符串中的位置

strings.Replace(str, old, new, n) string
// Replace 用于将字符串 str 中的前 n 个字符串 old 替换为字符串 new,并返回一个新的字符串,如果 n = -1 则替换所有字符串 old 为字符串 new

strings.Count(s, str string) int
// Count 用于计算字符串 str 在字符串 s 中出现的非重叠次数

strings.Repeat(s, count int) string
// Repeat 用于重复 count 次字符串 s 并返回一个新的字符串

strings.TrimSpace(s) 来剔除字符串开头和结尾的空白符号;如果你想要剔除指定字符,则可以使用 strings.Trim(s, "cut") 来将开头和结尾的 cut 去除掉。

strings.Split(s, sep) 用于自定义分割符号来对指定字符串进行分割,同样返回 slice。

strings.Join(sl []string, sep string) string
// Join 用于将元素类型为 string 的 slice 使用分割符号来拼接组成一个字符串

与字符串相关的类型转换都是通过 strconv 包实现的。

1
2
3
4
5
6
7
8
9
10
strconv.Itoa(i int) string
// 返回数字 i 所表示的字符串类型的十进制数。

strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int) string
// 将 64 位浮点型的数字转换为字符串

strconv.Atoi(s string) (i int, err error)
// 将字符串转换为 int 型。

strconv.ParseFloat(s string, bitSize int) (f float64, err error) // 将字符串转换为 float64 型。

第五章:控制结构

if-else结构

1
2
3
4
5
if condition {
//
} else {
//
}

当 if 结构内有 break、continue、goto 或者 return 语句时,Go 代码的常见写法是省略 else 部分

1
2
3
4
if condition {
return x
}
return y
1
2
3
4
if err != nil {
fmt.Printf("Program stopping with error %v", err)
os.Exit(1)
}

swith结构

它可以接受任意形式的表达式:

1
2
3
4
5
6
7
8
switch var1 {
case val1:
...
case val2:
...
default:
...
}

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。

一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块。

继续执行后续分支的代码,可以使用 fallthrough 关键字

1
2
3
4
5
switch i {
case 0: fallthrough
case 1:
f() // 当 i == 0 时函数也会被调用
}

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

switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true),然后在每个 case 分支中进行测试不同的条件。

1
2
3
4
5
6
7
8
switch {
case i < 0:
f1()
case i == 0:
f2()
case i > 0:
f3()
}

for结构

基于计数器的迭代

1
2
3
4
5
6
7
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Printf("This is the %d iteration\n", i)
}
}

基于条件判断的迭代

1
2
3
4
5
6
7
8
9
package main
import "fmt"
func main() {
var i int = 5
for i >= 0 {
i = i - 1
fmt.Printf("The variable i is now: %d\n", i)
}
}

for-range结构

for ix, val := range coll { }

val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值

1
2
3
for pos, char := range str {
...
}

标签和goto

for、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
func main() {
LABEL1: // 一般建议使用全部大写字母
for i := 0; i <= 5; i++ {
for j := 0; j <= 5; j++ {
if j == 4 {
continue LABEL1
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}
}

第六章:函数

简介

函数是基本的代码块。函数编写的顺序是无关紧要的,最好把 main() 函数写在文件的前面。

简单的 return 语句也可以用来结束 for 死循环,或者结束一个协程(goroutine)。

Go里面的函数:

  • 普通函数
  • 匿名函数
  • 方法

除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。

函数参数、返回值以及它们的类型被统称为函数签名。

函数是一等值(first-class value):它们可以赋值给变量,就像 add := binOp 一样。

函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数

函数参数与返回值

函数定义时,它的形参一般是有名字的,不过我们也可以定义没有形参名的函数,只有相应的形参类型,就像这样:func f(int, int, float64)

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

命名返回值

命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。

1
2
3
4
5
6
func getX2AndX3_2(input int) (x2 int, x3 int) {
x2 = 2 * input
x3 = 3 * input
// return x2, x3
return
}

改变外部变量

传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"fmt"
)
// this function changes reply:
func Multiply(a, b int, reply *int) {
*reply = a * b
}
func main() {
n := 0
reply := &n
Multiply(10, 5, reply)
fmt.Println("Multiply:", *reply) // Multiply: 50
}

变长参数

函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
func main() {
x := min(1, 3, 2, 0)
fmt.Printf("The minimum is: %d\n", x)
slice := []int{7,9,3,5,1}
x = min(slice...)
fmt.Printf("The minimum in the slice is: %d", x)
}
func min(s ...int) int {
if len(s)==0 {
return 0
}
min := s[0]
for _, v := range s {
if v < min {
min = v
}
}
return min
}

变长参数的类型不相同

  1. 使用结构体

  2. 使用空接口

    使用默认的空接口 interface{},这样就可以接受任何类型的参数

new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。

new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针

make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作。

defer和追踪

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数。

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"

func P(){
fmt.Print("P")
}

func main(){
fmt.Print("111")
defer P()
fmt.Print("222")
}

当有多个 defer 行为被注册时,它们会以逆序执行

1
2
3
4
5
6
7
8
9
10
11
12
package mian
import (
"file")
func main(){
//关闭文件
defer file.close()
//解锁
mu.Lock()
defer mu.Unlock()
//关闭数据库连接
defer disconnectFromDB()
}

函数作为参数

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func add(i,j int) int {
return i+j
}
func b(c int, f func (i,j int) int ){
return f(c, 2)
}
func main(){
fmt.Print(b(1, add))
}

闭包

匿名函数的使用

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main (){
// 匿名函数 func(i, j int) int {return i+j}
// 将匿名函数赋值给变量 变量使用匿名函数
ter := func(i, j int) int {return i+j}
ter(2,3)
// 匿名函数直接的调用
func(a, b int) int {return a-b} (3, 2)
}

函数作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"

func add(j int) func (i int) int{
return func (i int) int{
return j +i
}
}

func main(){
add(12)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func add() func(i int) int {
var x int
fmt.Printf("x: %v\n", x)
return func(i int) int {
x += i
return x
}
}
func main() {
f := add()
fmt.Printf("f(1): %v\n", f(1))
fmt.Printf("f(2): %v\n", f(20))

}

闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。

计算函数的执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func add() {
for i := 0; i < 100; i++ {
fmt.Printf("i: %v\n", i)
}
}

func main() {
start := time.Now()
add()
end := time.Now()
fmt.Print(end.Sub(start))
}

第七章:数组与切片

数组

1
2
3
4
5
6
7
8
package main
import "fmt"
func main(){
var a [10]int //声明
for i := 0; i<10 ; i++ {
fmt.Print(a[i])
}
}

数组是 可变的

Go 语言中的数组是一种 值类型,所以可以通过 new() 来创建:

1
2
3
a := new([10]int)
var b = new([10]int)
var c [10]int

b的类型是*[10]int, c的类型是[10]int

初始化

1
2
3
4
5
6
7
package main
import "fmt"
func main(){
var a = [10]int{1,2,3,4}
var b = [...]int{5,6,7,8}
var c = [10]string{1:"ta", 2:"wei"} // key: value 语法
}

切片

切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。切片是一个 长度可变的数组

切片是可索引的,并且可以由 len() 函数获取长度。计算容量的函数 cap() 可以测量切片最长可以达到多少。

1
2
3
4
5
6
package main
import "fmt"
func main(){
var a []int // 不需要说明长度
var b = []int{1,2,3,4,5} // 初始化
}

make创建数组

1
2
3
4
5
6
package main
import "fmt"
func main(){
var a []int = make([]int, 10)
b := make([]int, 10)
}

1.slice、map以及channel都是golang内建的一种引用类型,三者在内存中存在多个组成部分, 需要对内存组成部分初始化后才能使用,而make就是对三者进行初始化的一种操作方式

2. new 获取的是存储指定变量内存地址的一个变量,对于变量内部结构并不会执行相应的初始化操作, 所以slice、map、channel需要make进行初始化并获取对应的内存地址,而非new简单的获取内存地址

评论