包管理工具govendor
安装
- 下载 govendor
go get -u github.com/kardianos/govendor // 下载之后会在
$GOPATH/src
的相关路径下
注意: 在go包管理中,包会按目录(或者理解为 来源/用户)分开,govendor的相关路径为 $GOPATH/src/github.com/kardianos/govendor下,github.com
是来源,kardianos
是github上的用户,govendor
是项目名.也可以这样理解, 把 $GOPATH/src
下的目录拼接起来,则可以找到它的真正来源.
编译
- 进入目录
cd $GOPATH/src/github.com/kardianos/govendor
- 执行编译操作
go build
- 设置环境变量
export PATH=$PATH:/usr/local/go/bin/src/github.com/kardianos/govendor
- 检查
govendor –help
参数 | 解释 |
---|---|
-o | output 指定编译输出的名称,代替默认的包名 |
-i | install 安装作为目标的依赖关系的包(用于增量编译提速) |
-a | 强行对所有涉及到的代码包(包含标准库中的代码包)进行重新构建,即使它们已经是最新的了 |
-n | 仅打印输出build需要的命令,不执行build动作(少用) |
-p n | 指定编译过程中执行各任务的并行数量(确切地说应该是并发数量)。在默认情况下,该数量等于CPU的逻辑核数。但是在darwin/arm平台(即iPhone和iPad所用的平台)下,该数量默认是1 |
-race | 开启竞态条件的检测。不过此标记目前仅在linux/amd64、freebsd/amd64、darwin/amd64和windows/amd64平台下受到支持 |
-msan | 启用与内存消毒器的互操作。仅支持linux / amd64,并且只用Clang / LLVM作为主机C编译器(少用) |
-v | 打印出那些被编译的代码包的名字 |
-work | 打印出编译时生成的临时工作目录的路径,并在编译结束时保留它。在默认情况下,编译结束时会删除该目录 |
-x | 打印编译期间所用到的其它命令。注意它与-n标记的区别 |
-asmflags ‘flag list’ | 此标记可以后跟另外一些标记,如-D、-I、-S等。这些后跟的标记用于控制Go语言编译器编译汇编语言文件时的行为 |
-buildmode mode | 此标记用于指定编译模式,使用方式如-buildmode=default(这等同于默认情况下的设置)。此标记支持的编译模式目前有6种。借此,我们可以控制编译器在编译完成后生成静态链接库(即.a文件,也就是我们之前说的归档文件)、动态链接库(即.so文件)或/和可执行文件(在Windows下是.exe文件), go help buildmode |
-compiler name | 此标记用于指定当前使用的编译器的名称。其值可以为gc或gccgo。其中,gc编译器即为Go语言自带的编辑器,而gccgo编译器则为GCC提供的Go语言编译器。而GCC则是GNU项目出品的编译器套件。GNU是一个众所周知的自由软件项目。在开源软件界不应该有人不知道它 |
-gccgoflags ‘arg list’ | 此标记用于指定需要传递给gccgo编译器或链接器的标记的列表 |
-gcflags ‘arg list’ | 此标记用于指定需要传递给go tool compile命令的标记的列表 |
-installsuffix suffix | 为了使当前的输出目录与默认的编译输出目录分离,可以使用这个标记。此标记的值会作为结果文件的父目录名称的后缀。其实,如果使用了-race标记,这个标记会被自动追加且其值会为race。如果我们同时使用了-race标记和-installsuffix,那么在-installsuffix标记的值的后面会再被追加_race,并以此来作为实际使用的后缀。 |
-ldflags ‘flag list’ | -X importpath.name =value,将importpath中名为name的字符串变量的值设置为value。注意,在Go 1.5之前,这个选项有两个单独的参数。现在它在第一个=号上拆分一个参数。 -s -w: 压缩编译后的体积 -s: 去掉符号表 -w: 去掉调试信息,不能gdb调试了 |
-linkshared | 链接到以前使用创建的共享库 -buildmode=shared. |
-pkgdir dir | 使用此标记可以指定一个目录。编译器会只从该目录中加载代码包的归档文件,并会把编译可能会生成的代码包归档文件放置在该目录下 |
-tags ‘tag list’ | 此标记用于指定在实际编译期间需要受理的编译标签(也可被称为编译约束)的列表 |
-toolexec ‘cmd args’ | 此标记可以让我们去自定义在编译期间使用一些Go语言自带工具(如vet、asm等)的方式 |
包类型
状态 | 缩写状态 | 含义 |
---|---|---|
+local | l | 本地包,即项目自身的包组织 |
+external | e | 外部包,即被 $GOPATH 管理,但不在 vendor 目录下 |
+vendor | v | 已被 govendor 管理,即在 vendor 目录下 |
+std | s | 标准库中的包 |
+unused | u | 未使用的包,即包在 vendor 目录下,但项目并没有用到 |
+missing | m | 代码引用了依赖包,但该包并没有找到 |
+program | p | 主程序包,意味着可以编译为执行文件 |
+outside | 外部包和缺失的包 | |
+all | 所有的包 |
使用
注意:项目目录(工作空间)必须在 $GOPATH/src
目录下
init 创建 vendor 文件夹和 vendor.json 文件
list 列出已经存在的依赖包
add 从 $GOPATH 中添加依赖包,会加到 vendor.json
update 从 $GOPATH 升级依赖包
remove 从 vendor 文件夹删除依赖
status 列出本地丢失的、过期的和修改的package
fetch 从远端库增加新的,或者更新 vendor 文件中的依赖包
sync Pull packages into vendor folder from remote repository with revisions
migrate Move packages from a legacy tool to the vendor folder with metadata.
get 类似 go get,但是会把依赖包拷贝到 vendor 目录
license List discovered licenses for the given status or import paths.
shell Run a "shell" to make multiple sub-commands more efficient for large projects.
go tool commands that are wrapped:
`+<status>` package selection may be used with them
fmt, build, install, clean, test, vet, generate, tool
说明
govendor只是用来管理项目的依赖包,如果GOPATH中本身没有项目的依赖包,则需要通过go get先下载到GOPATH中
包导入
包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。
格式化代码工具
- gofmt
- goimports
占位符
定义示例类型和变量
type Human struct {
Name string
}
var people = Human{Name:"zhangsan"}
普通占位符
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%v | 相应值的默认格式 | Printf(“%v”, people) | {zhangsan} |
%+v | 打印结构体时,会添加字段名 | Printf(“%+v”, people) | {Name:zhangsan} |
%#v | 相应值的Go语法表示 | Printf(“#v”, people) | main.Human{Name:”zhangsan”} |
%T | 相应值的类型的Go语法表示 | Printf(“%T”, people) | main.Human |
%% | 字面上的百分号,并非值的占位符 | Printf(“%%”) | % |
布尔占位符
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%t | true 或 false | Printf(“%t”, true) | true |
整数占位符
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%b | 二进制表示 | Printf(“%b”, 5) | 101 |
%c | 相应Unicode码点所表示的字符 | Printf(“%c”, 0x4E2D) | 中 |
%d | 十进制表示 | Printf(“%d”, 0x12) | 18 |
%o | 八进制表示 | Printf(“%d”, 10) | 12 |
%q | 单引号围绕的字符字面值,由Go语法安全地转义 | Printf(“%q”, 0x4E2D) | ‘中’ |
%x | 十六进制表示,字母形式为小写 a-f | Printf(“%x”, 13) | d |
%X | 十六进制表示,字母形式为大写 A-F | Printf(“%x”, 13) | D |
%U | Unicode格式:U+1234,等同于 U+%|04X (说明: | 符号是为了防止转义,使用时需删除) |
Printf(“%U”, 0x4E2D) | U+4E2D |
浮点数和复数的组成部分(实部和虚部)
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%b | 无小数部分的,指数为二的幂的科学计数法,与 strconv.FormatFloat 的 ‘b’ 转换格式一致 | -123456p-78 | |
%e | 科学计数法,例如 -1234.456e+78 | Printf(“%e”, 10.2) | 1.020000e+01 |
%E | 科学计数法,例如 -1234.456E+78 | Printf(“%e”, 10.2) | 1.020000E+01 |
%f | 有小数点而无指数,例如 123.456 | Printf(“%f”, 10.2) | 10.200000 |
%g | 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0)输出 | Printf(“%g”, 10.20) | 10.2 |
%G | 根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的0)输出 | Printf(“%G”, 10.20+2i) | (10.2+2i) |
字符串与字节切片
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%s | 输出字符串表示(string类型或[]byte) | Printf(“%s”, []byte(“Go语言”)) | Go语言 |
%q | 双引号围绕的字符串,由Go语法安全地转义 | Printf(“%q”, “Go语言”) | “Go语言” |
%x | 十六进制,小写字母,每字节两个字符 | Printf(“%x”, “golang”) | 676f6c616e67 |
%X | 十六进制,大写字母,每字节两个字符 | Printf(“%X”, “golang”) | 676F6C616E67 |
指针
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%p | 十六进制表示,前缀 0x | Printf(“%p”, &people) | 0x4f57f0 |
其它标记
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
+ | 总打印数值的正负号;对于%q(%+q)保证只输出ASCII编码的字符 | Printf(“%+q”, “中文”) | “\u4e2d\u6587” |
- | 在右侧而非左侧填充空格(左对齐该区域) | ||
# | 备用格式:为八进制添加前导 0(%#o)为十六进制添加前导 0x(%#x)或 0X(%#X),为 %p(%#p)去掉前导 0x;如果可能的话,%q(%#q)会打印原始(即反引号围绕的)字符串;如果是可打印字符,%U(%#U)会写出该字符的Unicode 编码形式(如字符 x 会被打印成 U+0078 ‘x’)。 | Printf(“%#U”, ‘中’) | U+4E2D |
’ ‘ | (空格)为数值中省略的正负号留出空白(% d),以十六进制(% x, % X)打印字符串或切片时,在字节之间用空格隔开 | ||
0 | 填充前导的0而非空格;对于数字,这会将填充移到正负号之后 |
- golang没有 ‘%u’ 点位符,若整数为无符号类型,默认就会被打印成无符号的。
- 宽度与精度的控制格式以Unicode码点为单位。宽度为该数值占用区域的最小宽度;精度为小数点之后的位数。 操作数的类型为int时,宽度与精度都可用字符 ‘*’ 表示。
- 对于 %g/%G 而言,精度为所有数字的总数,例如:123.45,%.4g 会打印123.5,(而 %6.2f 会打印123.45)。
- %e 和 %f 的默认精度为6
- 对大多数的数值类型而言,宽度为输出的最小字符数,如果必要的话会为已格式化的形式填充空格。而以字符串类型,精度为输出的最大字符数,如果必要的话会直接截断。
new/make
- new
分配内存的内建函数,只是将其置零。也就是说,new(T)会为T类型的新项目,分配被置零的存储,并且返回它的地址,一个
类型为*T
的值。在Go的术语中,其返回一个指向新分配的类型为T的指针
,这个指针指向的内容的值
为零(zero value)。注意并不是指针为零。
- make
内建函数make(T, args)与new(T)的用途不一样。它只用来创建
slice
,map
和channel
,并且返回一个初始化的(而不是置零),类型为T的值(而不是*T)。之所以有所不同,是因为这三个类型的背后引用了使用前必须初始化的数据结构。例如,slice是一个三元描述符,包含一个指向数据(在数组中)的指针,长度,以及容量,在这些项被初始化之前,slice都是nil的。对于slice,map和channel,make初始化这些内部数据结构,并准备好可用的值。
符号、无符号
尽管Go语言提供了无符号数和运算,即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。事实上,内置的len函数返回一个有符号的int,我们可以像下面例子那样处理逆序循环。
medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
fmt.Println(medals[i]) // "bronze", "silver", "gold"
}
另一个选择对于上面的例子来说将是灾难性的。如果len函数返回一个无符号数,那么i也将是无符号的uint类型,然后条件i >= 0则永远为真。在三次迭代之后,也就是i == 0时,i–语句将不会产生-1,而是变成一个uint类型的最大值(可能是$2^64-1$),然后medals[i]表达式运行时将发生panic异常,也就是试图访问一个slice范围以外的元素。
出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。
字符串相关包
bytes
针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效
strings
字符串的查询、替换、比较、截断、拆分和合并等功能。
strconv
提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
unicode
提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。
数组
定义
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。
注意
- 只有数组的长度和类型都相同时才能进行比较,否则报语法错误
- 数组在函数参数传参中传递的是复制的副本,导致传递大的数组类型是低效的,修改操作也是会在副本上
- 显示的传数组指针,那么通过指针对数组的修改都可以返回到调用方
切片
定义
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice由三个部分构成:指针、长度和容量。
注意
- 指针指向第一个slice元素对应的底层数组元素的地址
- slice的第一个元素并不一定就是数组的第一个元素
- 长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置
- 内置的len和cap函数分别返回slice的长度和容量。
- 和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。
func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
// 运行的时间并不比支持==操作的数组或字符串更多
为何slice不直接支持比较运算符呢?
- 一个slice的元素是间接引用的,一个slice甚至可以包含自身。虽然有很多办法处理这种情形,但是没有一个是简单有效的。
- 因为slice的元素是间接引用的,一个固定的slice值(指slice本身的值,不是元素的值)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。
例如Go语言中map的key只做简单的浅拷贝,它要求key在整个生命周期内保持不变性(例如slice扩容,就会导致其本身的值/地址变化)。而用深度相等判断的话,显然在map的key这种场合不合适。对于像指针或chan之类的引用类型,==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的,也能临时解决map类型的key问题,但是slice和数组不同的相等测试行为会让人困惑。因此,安全的做法是直接禁止slice之间的比较操作。
if summer == nil { /* ... */ } // 唯一合法比较
Map
map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。
注意
- key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。
- 如果一个查找失败将返回value类型对应的零值
- 是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作。禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
- 和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现
- 向一个nil值的map存入元素将导致一个panic异常:
package main
import "fmt"
func main() {
var ages map[string]int
ages1 := make(map[string]int)
fmt.Println(ages == nil)
fmt.Println(len(ages) == 0)
fmt.Println(ages1 == nil)
fmt.Println(len(ages1) == 0)
ages1["alice"] = 31
fmt.Println(ages1)
}
结构体
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。 匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一个有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心
注意
- 结构体成员的输入顺序有重要的意义
- 如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。
- 结构体字面值并没有简短表示匿名成员的语法
函数
定义
函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符。形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。
每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,
Go语言没有默认参数值
,也没有任何方法可以通过参数名指定形参
,因此形参和返回值的变量名对于函数调用者而言没有意义。
return 返回值
当一个函数有多处return语句以及许多返回值时,bare return 可以减少代码的重复,但是使得代码难以被理解。 Go会将
返回值
在函数体的开始处
,根据它们的类型,将其初始化为它们对的false值
错误处理
在Go的错误处理中,错误是软件包API和应用程序用户界面的一个重要组成部分,程序运行失败仅被认为是几个预期的结果之一。 有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,正确的处理方式应该是先处理这些不完整的数据,再处理错误。因此对函数的返回值要有清晰的说明,以便于其他人使用。 Go这样设计的原因是由于对于某个应该在控制流程中处理的错误而言,将这个错误以异常的形式抛出会混乱对错误的描述,这通常会导致一些糟糕的后果。当某个程序错误被当作异常处理后,这个错误会将堆栈根据信息返回给终端用户,这些信息复杂且无用,无法帮助定位错误。 正因此,Go使用控制流机制(如if和return)处理异常,这使得编码人员能更多的关注错误处理。
错误处理策略
- 传播错误
主要用于子程序失败会变成该函数的失败
- 重新尝试失败的操作(注意重试次数和时间间隔)
适用于错误的发生是偶然的,或由不可预知的问题所导致的
- 打印日志并结束程序(应该只在main函数中执行)
适用于错误发生后,程序无法运行,在库函数中应该向上传播错误
if err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}
if err != nil {
log.Fatalf("Site is down: %v\n", err)
}
log.SetPrefix("wait: ") // 设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名。
log.SetFlags(0)
- 只打印错误信息
if err := Ping(); err != nil {
log.Printf("ping failed: %v; networking disabled",err)
}
// 或者标准错误流输出错误信息。
if err := Ping(); err != nil {
fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
}
- 直接忽略错误
应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该在清晰的记录下你的意图。
匿名函数
区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)。 当匿名函数需要被递归调用时,我们必须首先声明一个变量,再将匿名函数赋值给这个变量。
var visitAll func(items []string)
visitAll = func(items []string) {
for _, item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}
循环变量作用域
需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。
通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如下面的变量dir,虽然这看起来很奇怪,但却很有用。
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // NOTE: necessary!
os.MkdirAll(dir, 0755) // creates parent directories too
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}
// ...do some work…
for _, rmdir := range rmdirs {
rmdir() // clean up
}
for _, dir := range tempDirs() {
dir := dir // declares inner dir, initialized to outer dir
// ...
}
可变参数
调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调函数。如果原始参数已经是切片类型,在最后一个参数加上省略符 虽然在可变参数函数内部,…int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的。
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"
fmt.Println(sum(1, 2, 3, 4)) // "10"
func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"
deferred释放资源
只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当defer语句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。
func bigSlowOperation() {
defer trace("bigSlowOperation")() // don't forget the
extra parentheses
// ...lots of work…
time.Sleep(10 * time.Second) // simulate slow
operation by sleeping
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg,time.Since(start))
}
}
- 注意:
通过os.Create打开文件进行写入,在关闭文件时,我们没有对f.close采用defer机制,因为这会产生一些微妙的错误。许多文件系统,尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失,而我们还误以为写入操作成功。如果io.Copy和f.close都失败了,我们倾向于将io.Copy的错误信息反馈给调用者,因为它先于f.close发生,更有可能接近问题的本质。
- 在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。
Recover捕获异常
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
方法
定义
在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。
指针对象方法
一般会约定如果Point这个类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器 如果命名类型T(译注:用type xxx定义的类型)的所有方法都是用T类型自己来做接收器(而不是*T),那么拷贝这种类型的实例就是安全的;调用他的任何一个方法也就会产生一个值的拷贝。比如time.Duration的这个类型,在调用其方法时就会被全部拷贝一份,包括在作为参数传入函数的时候。但是如果一个方法使用指针作为接收器,你需要避免对其进行拷贝,因为这样可能会破坏掉该类型内部的不变性。比如你对bytes.Buffer对象进行了拷贝,那么可能会引起原始对象和拷贝对象只是别名而已,但实际上其指向的对象是一致的。紧接着对拷贝后的变量进行修改可能会有让你意外的结果。 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉C或者C++的人这里应该很快能明白。
封装
一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为
封装
。封装有时候也被叫做信息隐藏,同时也是面向对象编程最关键的一个方面。 Go语言只有一种控制可见性的手段
:大写首字母的标识符
会从定义它们的包中被导出
,小写字母的则不会
。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。
优点
- 调用方不能直接修改对象的变量值,只需要关注少量的语句并只要弄懂少量变量的可能值即可
- 隐藏实现的细节,防止调用方依赖那些可能变化的具体实现,这样设计包的程序员在不破坏对外的api情况下能得到更大的自由
- 阻止了外部调用放对对象内部值的任意进行修改
接口
接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。 一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。
对于创建的一个interface{}值持有一个boolean,float,string,map,pointer,或者任意其它的类型;我们当然不能直接对它持有的值做操作,因为interface{}没有任何方法。