作者:李骁
切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(和数组不一样)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口(这里有关动态窗口的含义,可参考数据库窗口函数的解释)。
切片是可索引的,并且可以由 len() 函数获取长度。
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个 长度可变的数组。
切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s 是一个切片,cap(s) 就是从 s[0] 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于 切片 s 来说该不等式永远成立:0 <= len(s) <= cap(s)。
多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。
优点
因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中切片比数组更常用。
注意
绝对不要用指针指向 slice,切片本身已经是一个引用类型,所以它本身就是一个指针!
声明切片的格式是: var identifier []type(不需要说明长度)。
一个切片在未初始化之前默认为 nil,长度为 0。
切片的初始化格式是:
var slice1 []type = arr1[start:end]
这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。
切片也可以用类似数组的方式初始化:
var x = []int{2, 3, 5, 7, 11}
这样就创建了一个长度为
作者:李骁
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。
数组长度也是数组类型的一部分,所以[5]int和[10]int是属于不同类型的。
注意事项:如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型。当使用值时我们必须先做一个类型判断。
数组元素可以通过 索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb。
如果每个元素是一个整型值,当声明数组时所有的元素都会被自动初始化为默认值 0。
遍历数组的方法既可以for 条件循环,也可以使用 for-range。这两种 for 结构对于切片(slices)来说也同样适用。
Go 语言中的数组是一种值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建:
var arr1 = new([5]int)
那么这种方式和 var arr2 [5]int 的区别是什么呢?arr1 的类型是 *[5]int,而 arr2的类型是 [5]int。
把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:
传递数组的指针
使用数组的切片
以上我们通常使用切片。
var arrLazy = [...]int{5, 6, 7, 8, 22}
这里是不定长数组, 其长度是根据初始化时指定的元素个数决定的。
几种赋值方式:
var arrAge = [5]int{18, 20, 15, 22, 16}
var arrLazy = [...]int{5, 6, 7, 8, 22}
var arrKeyValue = [5]string{3: "Chris", 4: "Ro
作者:李骁
Go 语言中的string类型存储的字符串是不可变的, 如果要修改string内容需要将string转换为[]byte或[]rune,并且修改后的string内容是重新分配的。
那么byte和rune的区别是什么(下面写法是type别名):
type byte = uint8
type rune = int32
string 类型的零值为长度为零的字符串,即空字符串 ""。
一般的比较运算符(==、!=、<、<=、>=、>)通过在内存中按字节比较来实现字符串的对比。你可以通过函数 len() 来获取字符串所占的字节长度,例如:len(str)。
字符串的内容(纯字节)可以通过标准索引法来获取,在中括号 [] 内写入索引,索引从 0 开始计数:
字符串 str 的第 1 个字节:str[0]
第 i 个字节:str[i - 1]
最后 1 个字节:str[len(str)-1]
需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。
注意事项:
获取字符串中某个字节的地址的行为是非法的,例如:&str[i]。
可以通过以下方式来对代码中多行的字符串进行拼接。
* 直接使用运算符
str := "Beginning of the string " +
"second part of the string"
由于编译器行尾自动补全分号的缘故,加号 + 必须放在第一行。
拼接的简写形式 += 也可以用于字符串:
s := "hel" + "lo, "
s += "world!"
fmt.Println(s) // 输出 “hello, world!”
里面的字符串都是不可变的,每次运算都会产生一个新的字符串,所以会产生很多临时的无用的字符串,不仅没有用,还会给 gc 带来额外的负担,所以性能比较差。
fmt.Sprintf("%d:%s", 2018, "年")
内部使用 []byte 实现,不像直接运算符这种会产生很多临时的字符串,但是内
作者:李骁
Go语言的算术运算符:
运算符 | 含义 | 示意 |
---|---|---|
+ | 相加 | A + B |
- | 相减 | A - B |
* | 相乘 | A * B |
/ | 相除 | B / A 结果还是整数 8/3=2 |
% | 求余 | B % A |
++ | 自增 | A++ 1 |
-- | 自减 | A-- |
Go语言的关系运算符:
运算符 | 含义 | 示意 |
---|---|---|
== | 检查两个值是否相等。 | (A == B) 为 False |
!= | 检查两个值是否不相等。 | (A != B) 为 True |
> | 检查左边值是否大于右边值。 | (A > B) 为 False |
< | 检查左边值是否小于右边值。 | (A < B) 为 True |
= |
检查左边值是否大于等于右边值。 | (A >= B) 为 False |
<= | 检查左边值是否小于等于右边值。 | (A <= B) 为 True |
Go语言的逻辑运算符:
运算符 | 操作 | 含义 |
---|---|---|
&& | 逻辑与 | 如果两边的操作数都是 True,则条件 True,否则为 False。 |
|| | 逻辑或 | 如果两边的操作数有一个 True,则条件 True,否则为 False。 |
! | 逻辑非 | 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 |
Go语言的位运算符:
位运算符对整数在内存中的二进制位进行操作。
下表列出了位运算符 &,|,和 ^ 的计算:
位 | 位 | & 与 | | 或 | ^ 异或 |
---|---|---|---|---|
p | q | p & q | p | q | p ^ q |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 |
作者:李骁
Go的工程项目管理非常简单,使用目录结构和package名来确定工程结构和构建顺序。
环境变量GOPATH在项目管理中非常重要,想要构建一个项目,必须确保项目目录在GOPATH中。而GOPATH可以有多个项目用";"分隔。
Go 项目目录下一般有三个子目录:
我们重点要关注的其实就是src文件夹中的目录结构。
为了进行一个项目,我们会在GoPATH目录下的src目录中,新建立一个项目的主要目录,比如我写的一个WEB项目《使用gin快速搭建WEB站点以及提供RestFull接口》。
https://github.com/ffhelicopter/tmm
项目主要目录“tmm”: $GoPATH/src/github.com/ffhelicopter/tmm
在这个目录(tmm)下面还有其他目录,分别放置了其他代码,大概结构如下:
src/github.com/ffhelicopter/tmm
/api
/handler
/model
/task
/website
main.go
main.go 文件中定义了package main 。同时也在文件中import了
"github.com/ffhelicopter/tmm/api"
"github.com/ffhelicopter/tmm/handler"
2个自定义包。
上面的目录结构是一般项目的目录结构,基本上可以满足单个项目开发的需要。如果需要构建多个项目,可按照类似的结构,分别建立不同项目目录。
当我们运行go install main.
作者:李骁
包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。
如同其它一些编程语言中的类库或命名空间的概念,每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。
你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main 。
package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。package main包下可以有多个文件,但所有文件中只能有一个main()方法,main()方法代表程序入口。
一个应用程序可以包含不同的包,而且即使你只使用 main 包也不必把所有的代码都写在一个巨大的文件里:你可以用一些较小的文件,并且在每个文件非注释的第一行都使用 package main 来指明这些文件都属于 main 包。如果你打算编译包名不是为 main 的源文件,如 pack1,编译后产生的对象文件将会是 pack1.a 而不是可执行程序。另外要注意的是,所有的包名都应该使用小写字母。当然,main包是不能在其他文档import的,编译器会报错:
import "xx/xx" is a program, not an importable package。
简单地说,在含有mian包的目录下,你可以写多个文件,每个文件非注释的第一行都使用 package main 来指明这些文件都属于这个应用的 main 包,只有一个文件能有mian() 方法,也就是应用程序的入口。main包不是必须的,只有在可执行的应用程序中需要。
一个 Go 程序是通过 import 关键字将一组包链接在一起。
import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。包名被封闭在半角双引号 "" 中。如果你打算从已编译的包中导入并加载公开声明的方法,不需要插入已编译包的源代
作者:李骁
包通过下面这个被编译器强制执行的规则来决定是否将自身的代码对象暴露给外部文件:
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
(大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)。
因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。
假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:
pack1.Thing //(pack1 在这里是不可以省略的)
因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如 pack1.Thing 和 pack2.Thing。
注意事项:
如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如 imported and not used: os,这正是遵循了 Go 的格言:“没有不必要的代码!”。
干净、可读的代码和简洁性是 Go 追求的主要目标。通过 Gofmt 来强制实现统一的代码风格。Go 语言中对象的命名也应该是简洁且有意义的。像 Java 和 Python 中那样使用混合着大小写和下划线的冗长的名称会严重降低代码的可读性。名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。返回某个对象的函数或方法的名称一般都是使用名词,没有 Get... 之类的字符,如果是用于修改某个对象,则使用 SetName。有必须要的话可以使用大小写混合的方式,如 MixedCaps 或 mixedCaps,而不是使
作者:李骁
局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
全局变量
作用域都是全局的(在本包范围内)
在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
全局变量可以在任何函数中使用。
简式变量
使用 := 定义的变量,如果新变量p与那个同名已定义变量 (这里就是那个全局变量p)不在一个作用域中时,那么Go 语言会新定义这个变量p,遮盖住全局变量p。刚开始很容易在此犯错而茫然,解决方法是局部变量尽量不同名。
注意,简式变量只能在函数内部声明使用,但是它可能会覆盖函数外全局同名变量。而且你不能在一个单独的声明中重复声明一个变量,但在多变量声明中这是允许的,而且其中至少要有一个新的声明变量。重复变量需要在相同的代码块内,否则你将得到一个隐藏变量。
如果你在代码块中犯了这个错误,将不会出现编译错误,但应用运行结果可能不是你所期望。所以尽可能避免和全局变量同名。
作者:李骁
常量使用关键字 const 定义,用于存储不会改变的数据。
存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量的定义格式:const identifier [type] = value,例如:
const Pi = 3.14159
在 Go 语言中,你可以省略类型说明符 [type],因为编译器可以根据变量(常量)的值来推断其类型。
显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"
一个没有指定类型的常量被使用时,会根据其使用环境而推断出它所需要具备的类型。换句话说,未定义类型的常量会在必要时刻根据上下文来获得相关类型。
常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
在这个例子中,iota 可以被用作枚举值:
const (
a = iota
b = iota
c = iota
)
第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
const (
a = iota
b
c
)
注意:
const (
a = iota
b = 8
c
)
a, b, c分别为0, 8, 2,可以简单理解为在一个const块中,每换一行定义个常量,iota 都会自动+1
( 关于 iota 的使用涉及到非常复杂多样的情况 ,这里不展开来讲了,有兴趣可以查查资料研究)
iota 也可以用在表达式中,如:iota + 50。在每遇到一个新的常量块或单个常量声明时, iota 都会重置为 0( 简单地讲,每遇到一次 const 关键字,iota 就重置为 0 )。
使用位左移与 iota 计数配合可优雅地实现存储单位的常量枚举:
type ByteSize float64
const (
_ = iota // 通过赋值给空白
作者:李骁
Go 语言变量名由字母、数字、下划线组成,其中首个字母不能为数字。
var (
a int
b bool
str string
)
这种因式分解关键字的写法一般用于声明全局变量,一般在func 外定义。
当一个变量被var声明之后,系统自动赋予它该类型的零值:
记住,这些变量在 Go 中都是经过初始化的。
多变量可以在同一行进行赋值,也称为 并行 或 同时 或 平行赋值。如:
a, b, c = 5, 7, "abc"
简式声明:
a, b, c := 5, 7, "abc" // 注意等号前的冒号
右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 5, b 的值是 7,c 的值是 "abc"。
简式声明一般用在func内,要注意的是:全局变量和简式声明的变量尽量不要同名,否则很容易产生偶然的变量隐藏Accidental Variable Shadowing。
即使对于经验丰富的Go开发者而言,这也是一个非常常见的陷阱。这个坑很容易挖,但又很难发现。
func main() {
x := 1
fmt.Println(x) // prints 1
{
fmt.Println(x) // prints 1
x := 2
fmt.Println(x) // prints 2
}
fmt.Println(x) // prints 1 (不是2)
}
如果你想要交换两个变量的值,则可以简单地使用:
a, b = b, a
(在 Go 语言中,这样省去了使用交换函数的必要)
空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7
中被抛弃。
_, b = 5, 7
_ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但