为什么要起这个名字?其实其中回答了何种人适合看这篇文章——有其他语言基础刚入web后端的初学者。本文主要是对黑马程序员的go基础教程部分做了精简,有基础的能节省不少时间。姊妹篇:菜鸟的最后一篇php教程。
写代码前需要知道的
示例代码:
// hello.go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello Go!")
}
-
要生成Go可执行程序,必须建立一个名字为main的包,并且在该包中包含一个叫main()的函数(该函数是Go可执行程序的执行起点)。
-
Go语言的main()函数不能带参数,也不能定义返回值。
-
强制左花括号{的放置位置,如果把左花括号{另起一行放置,这样做的结果是Go编译器报告编译错误。
命令与运行
只会编译代码,不运行可执行程序:
go build test.go
只会运行,不生成可执行程序:
go run test.go
基础类型
初始化
变量初始化的时候,支持如下的方式,注意(:=)不能用于声明之后的变量:
var v1 int = 10 // 方式1
var v2 = 10 // 方式2,编译器自动推导出v2的类型
v3 := 10 // 方式3,编译器自动推导出v3的类型
fmt.Println("v3 type is ", reflect.TypeOf(v3)) //v3 type is int
匿名变量
(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃:
_, i, _, j := 1, 2, 3, 4
func test() (int, string) {
return 250, "sb"
}
_, str := test()
iota枚举
在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一,下面是一些使用场景:
const (
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota"
)
const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0
const (
h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
)
const (
a = iota //a=0
b = "B"
c = iota //c=2
d, e, f = iota, iota, iota //d=3,e=3,f=3
g = iota //g = 4
)
const (
x1 = iota * 10 // x1 == 0
y1 = iota * 10 // y1 == 10
z1 = iota * 10 // z1 == 20
)
布尔类型
布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。
字符串
`(反引号)括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。
str2 := `hello
mike \n \r测试
`
fmt.Println("str2 = ", str2)
类型别名
type bigint int64 //int64类型改名为bigint
var x bigint = 100
type (
myint int //int改名为myint
mystr string //string改名为mystr
)
流程控制
range
关键字 range 会返回两个值,第一个返回值是元素的数组下标,第二个返回值是元素的值(支持 string/array/slice/map):
s := "abc"
for i := range s { //支持 string/array/slice/map。
fmt.Printf("%c\n", s[i])
}
for _, c := range s { // 忽略 index
fmt.Printf("%c\n", c)
}
for i, c := range s {
fmt.Printf("%d, %c\n", i, c)
}
函数
定义格式
func FuncName(/*参数列表*/) (o1 type1, o2 type2/*返回类型*/) {
//函数体
return v1, v2 //返回多个值
}
函数定义说明:
- func:函数由关键字 func 开始声明
- FuncName:函数名称,根据约定,函数名首字母小写即为private,大写即为public
- 参数列表:函数可以有0个或多个参数,参数格式为:变量名 类型,如果有多个参数通过逗号分隔,不支持默认参数
- 返回类型:
- 上面返回值声明了两个变量名o1和o2(命名返回参数),这个不是必须,可以只有类型没有变量名
- 如果只有一个返回值且不声明返回值变量,那么你可以省略,包括返回值的括号
- 如果没有返回值,那么就直接省略最后的返回信息
- 如果有返回值, 那么必须在函数的内部添加return语句
不定参数列表与传递
不定参数是指函数传入的参数个数为不定数量。为了做到这点,首先需要将函数定义为接受不定参数类型。形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数:
func Test(args ...int) {
for _, n := range args { //遍历参数列表
fmt.Println(n)
}
}
在传递参数时,一般有两种:
func Test(args ...int) {
MyFunc01(args...) //按原样传递, Test()的参数原封不动传递给MyFunc01
MyFunc02(args[1:]...) //Test()参数列表中,第1个参数及以后的参数传递给MyFunc02
}
关于函数的返回值命名
官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。命名了函数返回值的,返回时直接写return即可。
func Test02() (value int) { //方式2, 给返回值命名
value = 250
return value
}
func Test03() (value int) { //方式3, 给返回值命名
value = 250
return
}
函数类型
在Go语言中,函数也是一种数据类型,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型。
type FuncType func(int, int) int //声明一个函数类型, func后面没有函数名
//函数中有一个参数类型为函数类型:f FuncType
func Calc(a, b int, f FuncType) (result int) {
result = f(a, b) //通过调用f()实现任务
return
}
func Add(a, b int) int {
return a + b
}
func Minus(a, b int) int {
return a - b
}
func main() {
//函数调用,第三个参数为函数名字,此函数的参数,返回值必须和FuncType类型一致
result := Calc(1, 1, Add)
fmt.Println(result) //2
var f FuncType = Minus
fmt.Println("result = ", f(10, 2)) //result = 8
}
函数的返回值为匿名函数
// squares返回一个匿名函数,func() int
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
var x int
return func() int {//匿名函数
x++ //捕获外部变量
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}
函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用时匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。
通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。
延迟调用defer
关键字 defer ⽤于延迟一个函数或者方法(或者当前所创建的匿名函数)的执行。注意,defer语句只能出现在函数或方法的内部。如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执⾏。
工程管理
在实际的开发工作中,直接调用编译器进行编译和链接的场景是少而又少,因为在工程中不
会简单到只有一个源代码文件,且源文件之间会有相互的依赖关系。如果这样一个文件一个文件逐步编译,那不亚于一场灾难。 Go语言的设计者作为行业老将,自然不会忽略这一点。早期Go语言使用makefile作为临时方案,到了Go 1发布时引入了强大无比的Go命令行工具。
Go命令行工具的革命性之处在于彻底消除了工程文件的概念,完全用目录结构和包名来推导工程结构和构建顺序。针对只有一个源文件的情况讨论工程管理看起来会比较多余,因为这可以直接用go run和go build搞定。下面我们将用一个更接近现实的虚拟项目来展示Go语言的基本工程管理方法。
工作区
Go代码必须放在工作区中。工作区其实就是一个对应于特定工程的目录,它应包含3个子目录:src目录、pkg目录和bin目录。
- src目录:用于以代码包的形式组织并保存Go源码文件。(比如:.go .c .h .s等)
- pkg目录:用于存放经由go install命令构建安装后的代码包(包含Go库源码文件)的“.a”归档文件。
- bin目录:与pkg目录类似,在通过go install命令完成安装后,保存由Go命令源码文件生成的可执行文件。
目录src用于包含所有的源代码,是Go命令行工具一个强制的规则,而pkg和bin则无需手动创建,如果必要Go命令行工具在构建过程中会自动创建这些目录。
需要特别注意的是,只有当环境变量GOPATH中只包含一个工作区的目录路径时,go install命令才会把命令源码安装到当前工作区的bin目录下。若环境变量GOPATH中包含多个工作区的目录路径,像这样执行go install命令就会失效,此时必须设置环境变量GOBIN。
GOPATH设置
为了能够构建这个工程,需要先把所需工程的根目录加入到环境变量GOPATH中。否则,即使处于同一工作目录(工作区),代码之间也无法通过绝对代码包路径完成调用。
包
所有 Go 语言的程序都会组织成若干组文件,每组文件被称为一个包。这样每个包的代码都可以作为很小的复用单元,被其他项目引用。
一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径。
在 Go 语言里,命名为 main 的包具有特殊的含义。 Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。一个可执行程序有且仅有一个 main 包。
Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。
Go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。
包的一些操作
点操作:含义是这个包导入之后在你调用这个包的函数时,可以省略前缀的包名;
import . "fmt"
func main() {
Println("hello go")
}
别名操作:在导⼊时,可指定包成员访问⽅式,⽐如对包重命名,以避免同名冲突;
import io "fmt" //fmt改为为io
func main() {
io.Println("hello go") //通过io别名调用
}
空白标识符:有时,用户可能需要导入一个包,但是不需要引用这个包的标识符。在这种情况,可以使用空白标识符_来重命名这个导入。_操作可以理解为是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。
复合类型
类型 | 名称 | 长度 | 默认值 | 说明 |
pointer | 指针 | nil | ||
array | 数组 | 0 | ||
slice | 切片 | nil | 引⽤类型 | |
map | 字典 | nil | 引⽤类型 | |
struct | 结构体 |
指针
Go语言虽然保留了指针,但与其它编程语言不同的是:
- 默认值 nil,没有 NULL 常量
- 操作符 "&" 取变量地址, "*" 通过指针访问目标对象
- 不支持指针运算,不支持 "->" 运算符,直接⽤ "." 访问目标成员
new
表达式new(T)将创建一个T类型的匿名变量,所做的是为T类型的新值分配并清零一块内存空间,然后将这块内存空间的地址作为结果返回,而这个结果就是指向这个新的T类型值的指针值,返回的指针类型为*T。只需使用new()函数,无需担心其内存的生命周期或怎样将其删除。
func main() {
var p1 *int
p1 = new(int) //p1为*int 类型, 指向匿名的int变量
fmt.Println("*p1 = ", *p1) //*p1 = 0
p2 := new(int) //p2为*int 类型, 指向匿名的int变量
*p2 = 111
fmt.Println("*p2 = ", *p2) //*p1 = 111
}
数组
数组⻓度必须是常量,且是类型的组成部分。 [2]int 和 [3]int 是不同类型。内置函数 len(长度) 和 cap(容量) 都返回数组⻓度 (元素数量)。下面是一些初始化的操作:
a := [3]int{1, 2} // 未初始化元素值为 0
b := [...]int{1, 2, 3} // 通过初始化值确定数组长度
c := [5]int{2: 100, 4: 200} // 通过索引号初始化元素,未初始化元素值为 0
slice
切片并不是数组或数组指针,它通过内部指针和相关属性引⽤数组⽚段,以实现变⻓⽅案。slice并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层array,slice的声明也可以像array一样,只是不需要长度。
创建与初始化
slice和数组的区别:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。
var s1 []int //声明切片和声明array一样,只是少了长度,此为空(nil)切片
s2 := []int{}
//make([]T, length, capacity) //capacity省略,则和length的值相同
var s3 []int = make([]int, 0)
s4 := make([]int, 0, 0)
s5 := []int{1, 2, 3} //创建切片并初始化
注意:make只能创建slice、map和channel,并且返回一个有初始值(非零)。
内建函数
1) append
append函数向 slice 尾部添加数据,返回新的 slice 对象。append函数会智能地底层数组的容量增长,一旦超过原底层数组容量,通常以2倍容量重新分配底层数组,并复制原来的数据。
func main() {
s := make([]int, 0, 1)
c := cap(s)
for i := 0; i < 50; i++ {
s = append(s, i)
if n := cap(s); n > c {
fmt.Printf("cap: %d -> %d\n", c, n)
c = n
}
}
/*
cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64
*/
}
2) copy
函数 copy 在两个 slice 间复制数据,复制⻓度以 len 小的为准,两个 slice 可指向同⼀底层数组。
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := data[8:] //{8, 9}
s2 := data[:5] //{0, 1, 2, 3, 4}
copy(s2, s1) // dst:s2, src:s1
fmt.Println(s2) //[8 9 2 3 4]
fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9]
map
Go语言中的map(映射、字典)是一种内置的数据结构,它是一个无序的key—value对的集合,比如以身份证号作为唯一键来标识一个人的信息。map格式为:
map[keyType]valueType
在一个map里所有的键都是唯一的,而且必须是支持==和!=操作符的类型,切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误。map是无序的,我们无法决定它的返回顺序,所以,每次打印结果的顺利有可能不同。
创建与初始化
var m1 map[int]string //只是声明一个map,没有初始化, 此为空(nil)map
fmt.Println(m1 == nil) //true
//m1[1] = "mike" //err, panic: assignment to entry in nil map
//m2, m3的创建方法是等价的
m2 := map[int]string{}
m3 := make(map[int]string)
fmt.Println(m2, m3) //map[] map[]
m4 := make(map[int]string, 10) //第2个参数指定容量
fmt.Println(m4) //map[]
常用操作
1)赋值
m1 := map[int]string{1: "mike", 2: "yoyo"}
m1[1] = "xxx" //修改
m1[3] = "lily" //追加, go底层会自动为map分配空间
fmt.Println(m1) //map[1:xxx 2:yoyo 3:lily]
2)遍历
m1 := map[int]string{1: "mike", 2: "yoyo"}
//迭代遍历1,第一个返回值是key,第二个返回值是value
for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
//1 ----> mike
//2 ----> yoyo
}
//判断某个key所对应的value是否存在, 第一个返回值是value(如果存在的话)
value, ok := m1[1]
fmt.Println("value = ", value, ", ok = ", ok) //value = mike , ok = true
3)删除
m1 := map[int]string{1: "mike", 2: "yoyo", 3: "lily"}
//迭代遍历1,第一个返回值是key,第二个返回值是value
for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
//1 ----> mike
//2 ----> yoyo
//3 ----> lily
}
delete(m1, 2) //删除key值为3的map
注意!在函数间传递映射并不会制造出该映射的一个副本,不是值传递,而是引用传递。
结构体
当结构体变量为指针变量时,p.成员 和(*p).成员 操作是等价的。在进行值传递时,形参的修改不会影响到实参。Go语言对关键字的增加非常吝啬,其中没有private、 protected、 public这样的关键字。要使某个符号对其他包(package)可见(即可以访问),需要将该符号定义为以大写字母开头。
其他的内容,和C语言等其他语言类似,不再赘述。
面向对象编程
尽管Go语言中没有封装、继承、多态这些概念,但同样通过别的方式实现这些特性:
- 封装:通过方法实现
- 继承:通过匿名字段实现
- 多态:通过接口实现
匿名组合
所有的内置类型和自定义类型都是可以作为匿名字段的。当匿名字段也是一个结构体的时候,那么这个结构体所拥有的全部字段都被隐式地引入了当前定义的这个结构体。初始化如下:
//人
type Person struct {
name string
sex byte
age int
}
//学生
type Student struct {
Person // 匿名字段,那么默认Student就包含了Person的所有字段
id int
addr string
}
func main() {
//顺序初始化
s1 := Student{Person{"mike", 'm', 18}, 1, "sz"}
//s1 = {Person:{name:mike sex:109 age:18} id:1 addr:sz}
fmt.Printf("s1 = %+v\n", s1)
//s2 := Student{"mike", 'm', 18, 1, "sz"} //err
//部分成员初始化1
s3 := Student{Person: Person{"lily", 'f', 19}, id: 2}
//s3 = {Person:{name:lily sex:102 age:19} id:2 addr:}
fmt.Printf("s3 = %+v\n", s3)
//部分成员初始化2
s4 := Student{Person: Person{name: "tom"}, id: 3}
//s4 = {Person:{name:tom sex:0 age:0} id:3 addr:}
fmt.Printf("s4 = %+v\n", s4)
}
在访问Student中的元素时,如果没有同名字段,那么访问Person类中的方式与之前的相同。如果有,就需要显示调用。
方法
在Go语言中,可以给任意自定义类型(包括内置类型,但不包括指针类型)添加相应的方法。⽅法总是绑定对象实例,并隐式将实例作为第⼀实参 (receiver),方法的语法如下:
- 参数 receiver 可任意命名。如⽅法中未曾使⽤,可省略参数名。
- 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接⼝或指针。
- 不支持重载方法,也就是说,不能定义名字相同但是不同参数的方法。
- (未完待续)