Go 是一门类似 C 的编译型语言,但是它的编译速度非常快。这门语言的关键字总共也就二十五个,比英文字母还少一个,这对于我们的学习来说就简单了很多。先让我们看一眼这些关键字都长什么样:
下面列举了 Go 代码中会使用到的 25 个关键字或保留字:
break default func interface select case defer go map struct chan else
goto package switch const fall through if range type continue for
import return var
Go 程序设计的一些规则
Go 之所以会那么简洁,是因为它有一些默认的行为:
Go 语言里面定义变量有多种方式。
使用var
关键字是 Go 最基本的定义变量方式,与 C 语言不同的是 Go 把变量类型放在变量名后面:
//定义一个名称为“valName”,类型为"type"的变量
var valName type
定义多个变量:
//定义三个类型都是“type”的变量
var vname1, vname2, vname3 type
定义变量并初始化值:
//初始化“vName”的变量为“value”值,类型是“type”
var vName type = value
同时初始化多个变量:
/*
定义三个类型都是"type"的变量,并且分别初始化为相应的值
vname1为v1,vname2为v2,vname3为v3
*/
var vname1, vname2, vname3 type= v1, v2, v3
你是不是觉得上面这样的定义有点繁琐?没关系,因为 Go 语言的设计者也发现了,有一种写法可以让它变得简单一点。我们可以直接忽略类型声明,那么上面的代码变成这样了:
/*
定义三个变量,它们分别初始化为相应的值
vname1为v1,vname2为v2,vname3为v3
然后Go会根据其相应值的类型来帮你初始化它们
*/
var vname1, vname2, vname3 = v1, v2, v3
你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化:
/*
定义三个变量,它们分别初始化为相应的值
vname1为v1,vname2为v2,vname3为v3
编译器会根据初始化的值自动推导出相应的类型
*/
vname1, vname2, vname3 := v1, v2, v3
:=
这个符号直接取代了 var
和 type
,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用 var
方式来定义全局变量。
_
(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值2
赋予b
,并同时丢弃1
:
_, b := 1, 2
Go 对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了i
但未使用:
package main
func main() {
var i int
}
所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在 Go 程序中,常量可定义为数值、布尔值或字符串等类型。
它的语法如下:
const constantName = value
//如果需要,也可以明确指定常量的类型:
const Pi float32 = 3.1415926
下面是一些常量声明的例子:
const PI = 3.1415926
const MaxThread = 10
const Prefix = "so_"
Go 语言按类别有以下几种数据类型:
bool
,值只可以是常量 true 或者 false。一个简单的例子:var b bool = true
;Boolean
//示例代码
var isActive bool // 全局变量声明
var enabled, disabled = true, false // 忽略类型的声明
func test() {
var available bool // 一般声明
valid := false // 简短声明
available = true // 赋值操作
}
数值类型
整数类型有无符号和带符号两种。Go 同时支持int
和uint
,这两种类型的长度相同,但具体长度取决于不同编译器的实现。
Go 里面也有直接定义好位数的类型:rune
, int8
, int16
, int32
, int64
和byte
, uint8
, uint16
, uint32
, uint64
。其中rune
是int32
的别称,byte
是uint8
的别称。
“ 需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。
如下的代码会产生错误:invalid operation: a + b (mismatched types int8 and int32)
“ var a int8
var b int32
c:=a + b
另外,尽管 int 的长度是 32 bit, 但 int 与 int32 并不可以互用。
浮点数的类型有float32
和float64
两种(没有float
类型),默认是float64
。
这就是全部吗?No!Go 还支持复数。它的默认类型是 complex128
(64 位实数+64 位虚数)。如果需要小一些的,也有complex64
(32 位实数+32 位虚数)。复数的形式为RE + IMi
,其中RE
是实数部分,IM
是虚数部分,而最后的i
是虚数单位。下面是一个使用复数的例子:
var c complex64 = 5+5i
//output: (5+5i)
fmt.Printf("Value is: %v", c)
字符串
Go 中的字符串都是采用UTF-8
字符集编码,字符串是用一对双引号(""
)或反引号(` `)括起来定义,它的类型是 string
。
func test(a,b int) {
str := "hello world"
m := "haha"
result := str + m
println(result)
multStr := `hello
world`
println(multStr)
}
在 Go 中字符串是不可变的,例如下面的代码编译时会报错:cannot assign to s[0]
var s string = "hello"
s[0] = 'k'
如果你想要修改一个字符串怎么办呢:
s := "hello"
c := []byte(s)
c[0] = 'c'
s1 := string(c)
println(s1)
Go 中可以使用 + 操作符来连接两个字符串:
str := "hello world"
m := "haha"
result := str + m
println(result)
如果要声明一个多行的字符串怎么办?可以通过`` 来声明:
multStr := `hello
world`go
println(multStr)
括起的字符串为 Raw 字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。例如本例中会输出:
hello
world
错误类型
Go 内置有一个error
类型,专门用来处理错误信息,Go 的package
里面还专门有一个包errors
来处理错误:
func error() {
e := errors.New("this is a error demo")
if e != nil {
println(e)
}
}
这个关键字用来声明enum
的时候采用,它默认开始值是 0,const 中每增加一行加 1:
package main
const (
x = iota // x == 0
y
z
w
)
const (
h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
)
const (
l = iota //a=0
m = "B"
n = iota //2
o, p, q = iota, iota, iota //3,3,3
r = iota //4
)
func main() {
println(x,y,z,w)
println(h,i,j)
println(l,m,n,o,p,q,r)
}
结果:
0 1 2 3
0 0 0
0 B 2 3 3 3 4
array 就是数组,定义方式如下:
var arr [n]type
在[n]type
中,n
表示数组的长度,type
表示存储元素的类型。对数组的操作和其它语言类似,都是通过[]
来进行读取或赋值:
var arr [10]int // 声明了一个int类型的数组
arr[0] = 1 // 数组下标是从0开始的
arr[1] = 2 // 赋值操作
由于长度也是数组类型的一部分,因此[3]int
与[4]int
是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的slice
类型了。
数组可以使用另一种:=
来声明:
a := [3]int{
1, 2, 3} // 声明了一个长度为3的int数组
b := [10]int{
1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0
c := [...]int{
4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度
Go 支持嵌套数组,即多维数组。比如下面的代码就声明了一个二维数组:
// 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素
doubleArray := [2][4]int{[4]int{
1, 2, 3, 4}, [4]int{
5, 6, 7, 8}}
// 上面的声明可以简化,直接忽略内部的类型
easyArray := [2][4]int{
{
1, 2, 3, 4}, {
5, 6, 7, 8}}
注意: [2][4] int 表示的是一个 int 型数组,数组内有两个数组,每个数组有四个元素组成。
知道 python 数组的就知道 slice,跟 python 的实现是一样的。
slice 有一些简便的操作:
slice
的默认开始位置是 0,ar[:n]
等价于 ar[0:n]
slice
的第二个序列默认是数组的长度,ar[n:]
等价于ar[n:len(ar)]
slice
,可以这样 ar[:]
,因为默认第一个序列是 0,第二个是数组的长度,即等价于ar[0:len(ar)]
下面有一些关于 slice 的示例:
// 声明一个数组
var array = [10]byte{
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个slice
var aSlice, bSlice []byte
// 演示一些简便操作
aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c
aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素
// 从slice中获取slice
aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7
bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h
bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g
重要:slice
是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的aSlice
和bSlice
,如果修改了aSlice
中元素的值,那么bSlice
相对应的值也会改变。
对于slice
有几个有用的内置函数:
len
获取slice
的长度cap
获取slice
的最大容量append
向slice
里面追加一个或者多个元素,然后返回一个和slice
一样类型的slice
copy
函数copy
从源slice
的src
中复制元素到目标dst
,并且返回复制的元素的个数//1.基于数组创建数组切片
var array = [10]int{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var slice = array[1:7] //array[startIndex:endIndex] 不包含endIndex
//2.直接创建数组切片
slice2 := make([]int, 5, 10)
//3.直接创建并初始化数组切片
slice3 := []int{
1, 2, 3, 4, 5, 6}
//4.基于数组切片创建数组切片
slice5 := slice3[:4]
println(slice5)
//5.遍历数组切片
for i, v := range slice3 {
println(i, v)
}
//6.len()和cap()
var len = len(slice2) //数组切片的长度
var cap = cap(slice) //数组切片的容量
println("len(slice2) =", len)
println("cap(slice) =", cap)
//7.append() 会生成新的数组切片
slice4 := append(slice2, 6, 7, 8)
slice4 = append(slice4, slice3...)
println(slice4)
//8.copy() 如果进行操作的两个数组切片元素个数不一致,将会按照个数较小的数组切片进行复制
copy(slice2, slice3) //将slice3的前五个元素复制给slice2
println(slice2, slice3)
map
map 就是 Java 中的 Map,python 中的字典。它的格式为 map[keyType]valueType
。
fruit := map[string]int{
"apple":5,"orange":7,"pineapple":3}
println(fruit)
var appleCount = fruit["apple"]
println(appleCount)
使用 map 过程中需要注意的几点:
map
是无序的,每次打印出来的map
都会不一样,它不能通过index
获取,而必须通过key
获取map
的长度是不固定的,也就是和slice
一样,也是一种引用类型len
函数同样适用于map
,返回map
拥有的key
的数量map
的值可以很方便的修改,通过numbers["one"]=11
可以很容易的把 key 为one
的字典值改为11
map
和其他基本型别不同,它不是 thread-safe,在多个 go-routine 存取时,必须使用 mutex lock 机制map
的初始化可以通过key:val
的方式初始化值,同时map
内置有判断是否存在key
的方式
make
用于内建类型(map
、slice
和channel
)的内存分配。new
用于各种类型的内存分配。
内建函数new
本质上说跟其它语言中的同名函数功能一样:new(T)
分配了零值填充的T
类型的内存空间,并且返回其地址,即一个*T
类型的值。用 Go 的术语说,它返回了一个指针,指向新分配的类型T
的零值。有一点非常重要:
“
new
返回指针。
内建函数make(T, args)
与new(T)
有着不同的功能,make 只能创建slice
、map
和channel
,并且返回一个有初始值(非零)的T
类型,而不是*T
。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice
,是一个包含指向数据(内部array
)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice
为nil
。对于slice
、map
和channel
来说,make
初始化了内部的数据结构,填充适当的值。
“
make
返回初始化后的(非零)值。
对于不同的数据类型,零值的意义是完全不一样的。比如,对于 bool 类型,零值为 false;int 的零值为 0;string 的零值是空字符串:
b := new(bool)
println(*b)
i := new(int)
println(*i)
s := new(string)
println(*s)
输出:
false
0
注意:上面最后 string 的输出是空值。
关于 “零值”,所指并非是空值,而是一种 “变量未填充前” 的默认值,通常为 0。 此处罗列部分类型的 “零值”:
int 0
int8 0
int32 0
int64 0
uint 0x0
rune 0 //rune的实际类型是 int32
byte 0x0 // byte的实际类型是 uint8
float32 0 //长度为 4 byte
float64 0 //长度为 8 byte
bool false
string ""
1.分组声明
在 Go 语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。
例如下面的代码:
import "log"
import "net/http"
import "strings"
const a = 3
const b = 2
const c = 4
var str = "aa"
var prefix = "abc_"
可以写成如下分组形式:
import (
"log"
"net/http"
"strings"
)
const(
a = 2,
b = 2,
c = 4
)
var(
str = "aa"
prefix = "abc_"
)
Go 中流程控制分三大类:条件判断,循环控制和无条件跳转。
Go 里面if
条件判断语句中不需要括号,如下代码所示 :
if x > 80{
println("better")
} else {
println("good")
}
Go 的if
还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示
if countScore := getCountScore(); countScore >= 80 {
println("better")
} else if countScore >= 60 {
println("good")
} else {
println("e......")
}
//这里打印countScore是找不到这个变量的
println(countScore)
Go 有goto
语句——但是请明智地使用它。用goto
跳转到必须在当前函数内定义的标签。例如假设这样一个循环:
a := 4
b := 5
if a > b {
println(a * b)
} else {
Ding:
a = a+12
b = a + (32/4)
if a < b {
goto Ding
}
}
注意:标签名是大小写敏感的
Go 里面的 for 除了基本的循环外,还可以读取 slice 和 map 的数据:
sum := 0
for i:=0;i<10;i++ {
sum += i
}
for
配合range
可以用于读取slice
和map
的数据:
fruit := map[string]int{
"apple":5,"orange":7,"pineapple":3}
for k,v := range fruit{
println(k,v)
}
小插曲:
"_" 的使用
由于 Go 支持 “多值返回”, 而对于 “声明而未被调用” 的变量,编译器会报错,在这种情况下可以使用 _
来丢弃不需要的返回值 例如 :
fruit := map[string]int{
"apple":5,"orange":7,"pineapple":3}
for _,v := range fruit{
println(v)
}
"_"相当于占位符的作用,这个位置必须要有一个值来接收,但是这个值又没有用,可以用 “ _”来占着位置。
跟别的语言中的 switch 别无他致:
var sex byte
switch sex {
case 0:
println("男生")
case 1:
println("女生")
default:
println("纳尼。。。。。")
}
函数是 Go 里面的核心设计,它通过关键字func
来声明,它的格式如下:
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
//返回多个值
return value1, value2
}
上面的代码我们看出:
func
用来声明一个函数funcName
,
分隔output1
和output2
,如果你不想声明也可以,直接就两个类型来看一个最简单的函数:
package main
func add(a,b int) map[] {
var resultMap = map[string]int{}
resultMap["add"] = a + b
resultMap["multi"] = a * b
resultMap["subtract"] = a - b
resultMap["division"] = a / b
return resultMap
}
func main() {
a := 3
b := 4
count := add(a, b)
println(count)
}
多个返回值:
Go 中函数可以有多个返回值,这个比 Java 强悍 100 倍:
package main
func add(a,b int) (int,int) {
return a+b,a-b
}
func main() {
a := 3
b := 4
add,sub := add(a, b)
println(add,sub)
}
此时 Java 中的各路神仙还在各大论坛发帖:使用 Map 处理多个返回值我见一个打一个。
可变参数:
跟 Java 中差不多吧:
func myFunc(arg ...int) {}
defer
defer 是 golang 的一个特色功能,被称为“延迟调用函数”。当外部函数返回后执行 defer。类似于其他语言的 try… catch … finally…
中的 finally,当然差别还是明显的。
释放占用资源:
func test() error {
file, err := os.Open("path")
if err != nil {
return err
}
//放在判断err状态之后
defer file.Close()
//todo
//...
return nil
//defer执行时机
}
异常处理:
func test2() {
defer func() {
if err := recover(); err != nil {
println(err)
}
}()
file, err := os.Open("path")
if err != nil {
panic(err)
}
defer file.Close()
//todo
//...
return
//defer执行时机
}
日志输出:
func test3() {
t1 := time.Now()
defer func() {
println("耗时: %f s", time.Now().Sub(t1).Seconds())
}()
//todo
//...
return
//defer执行时机
}
Java 中我们会去声明一些 bean 对象,里面包含字段和属性,在 Go 中可以声明一个 struct 类型的实体,在这个实体中声明一些属性,但是不可以在里面定义 func。
type person struct {
name string
age int
}
使用方式:
package main
type person struct {
name string
age int
}
func main() {
var p person
p.age = 13
p.name = "xiaoming"
println(p.name,p.age)
p1 := person{
"xiaoming",13}
println(p1.name,p1.age)
p2 := person{
age:13,name:"xiaoming"}
println(p2.name,p2.age)
}
Go 中的继承---匿名字段
上面我们在定义 struct 的时候,里面的属性都是字段名和类型一一对应的。实际上 Go 也支持只提供类型而不写字段名的方式,也就是匿名字段。
当匿名字段是一个 struct 的时候,那么这个 struct 所拥有的全部字段都被隐式地引入了当前定义的这个 struct。
package main
type PersonOther struct {
phone string
address string
}
type Person struct {
PersonOther
name string
age int
}
func main() {
p1 := Person{
PersonOther{
"13242342123","xxxxxxx"},"xiaoming",13}
println(p1.address,p1.phone,p1.name,p1.age)
}
可以看到在 p1 中是可以看到 address 和 phone 属性的,这就跟 Java 中的继承一样。
既然说到继承,那么肯定会有这样的一种情况,就是在 Person 和 PersonOther 中都定义过了 phone 属性,当我们用 p1 去获取的时候到底获取的是哪个对象的 phone 属性呢?
Go 里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过 Person.phone
访问的时候,是访问 student 里面的字段,而不是 PersonOther 里面的字段。
当前如果你想访问父类中的 phone 也不是不可以,Go 还保留着父类中的对象呢,你可以这样取出来:
parentPhone := p1.PersonOther.phone
在 Java 中如果是这样的话,父类的同名字段就被子类覆写了,取不出来。
在 Java 中我们经常这样做:
定义一个关于计算面积的接口;
定义一个计算圆面积的类实现接口;
定义一个正方形计算面积的类实现接口;
定义一个计算长方形面积的类实现接口;
......
这样我们就抽象出来一套统一的计算面积的方案由一个接口把持,需要哪个面积计算就调用相关的实现。
在 Go 中同样也有这样的概念,但是实现方式确是不同。基于上面的原因所以就有了method
的概念,method
是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在func
后面增加了一个 receiver(也就是 method 所依从的主体)。
method 的语法如下:
func (r ReceiverType) funcName(parameters) (results)
看一个具体的示例:
package main
type Circle struct {
redius float64
}
type Square struct {
width,height float64
}
func (c Circle) area() float64 {
return c.redius * c.redius * 3.14
}
func (s Square) area() float64 {
return s.height * s.width
}
func main() {
c := Circle{
3.55}
s := Square{
3,5}
println(c.area())
println(s.area())
}
可以看到调用 method 通过 Circle 示例访问就像访问 struct 里面的字段一样。
各位看官到这里已经把 Go 的基本语法知识点大略的过了一遍,可能大家第一遍看觉得好简单,但是让你写个 Hello world 你可能又写不出来。别怕,贵在动手,把所有的语法知识都敲一遍,自然就熟悉了,就像你当初刚刚学习编程一样,只不过是从头再来,加油你可以的!
关注我不迷路