写在前面:本系列为java程序员转go的快速学习笔记,涉及基础语法、进阶知识、简单实战三大部分
基础语法
本节主要讲解 go 语言的数据类型、变量的申明和赋值、条件语句、循环语句。
一、数据类型
和大多数后端语言一样,go 语言数据类型也可分为基本数据类型和引用数据类型
go 语言基本数据类型如下:
布尔型:go 用 bool 表示
整型:Go 里面有 int8 int16 int32 uint64 ,分别对应 Java 中的 byte short int long,同时 Go 里面还有专门 表示无符号数的 uint8 uint 16 uint32 uint64。
浮点型:float32 float64
字符串类型: string
很多写其他后端语言如 Java 的同学刚开始接触 go 可能会对 uint8 和 rune 不太清楚,其实 []uint8 即 []byte,rune 通常表示 int32 ,也就是4个字节有符号的整数,对应 Java 中 int
byte // uint8 的别名
rune // int32 的别名
除此之外,go 语言数据类型一些派生类型,包括:
指针类型(Pointer)、数组类型、结构化类型(struct)、Channel 类型、函数类型、切片类型、接口类型(interface)、Map 类型 等。
二、变量
变量申明赋值
Go 语言变量申明赋值有三种方式
a. 先声明、后赋值
例如 :
var name string
name = "lubanproj"
Go 语言中用 var 申明变量,且行末没有“ ; ” 。C++ 和 Java 语言中是以 分号 “ ; ” 进行语句分割,所以行末会有分号。
和大多数语言约定不同, Go 中申明变量,变量放在类型前,例如上文 var name string,如果从 C++ 和 Java 等语言刚切换过来可能会很不习惯,这里一定要注意
b.申明并且赋值
var name = "lubanproj"
// 编译器会根据值自行判定变量类型
c.使用 " := " 申明并赋值
name := "lubanproj"
// 这里省略 var 关键字 ,效果和第二种是一样的
三、条件语句
Go 语言中 if 语句的语法如下:
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}
需要注意的是,Go 中条件语句,if 没有括号 , 例如:
if a == 10 {
/* 如果条件为 true 则执行以下语句 */
fmt.Println("a == 10" )
}
此外,if 还有另外一种形式,它包含一个 statement
可选语句部分,该组件在条件判断之前运行。它的语法是
if statement; condition {
}
例如:
if a := 1 ; a < 10 {
/* 如果条件为 true 则执行以下语句 */
fmt.Println("a < 10" )
}
四、循环语句
与其他后台语言不同的是,Go 语言只提供了 for 循环,没有 while 循环。
go 虽然没有提供 while 循环,但是可以使用 for 循环实现 其他语言 while 循环的效果。
Go 语言的 for 循环有 3 种形式,只有其中的一种使用分号。
第一种 :和其他语言的 for 语法类似,语法格式如下 :
for init; condition; post {
}
init: 一般为赋值表达式,给控制变量赋初值;
condition: 关系表达式或逻辑表达式,循环控制条件;
post: 一般为赋值表达式,给控制变量增量或减量。
例如 :
for i :=1 ; i < 10 ; i ++ {
fmt.Println("i = ", i)
}
这里是不是很熟悉? 跟其他语言唯一不一样的地方在于 Go 中 for 循环没有小括号。
第二种 :和其他语言 while 循环 类似,语法格式如下 :
for condition { }
例如 :
for 0 == 0 {}
for true {}
第三种 : 死循环,类似 while (true) {} , 语法格式如下:
for { }
例如 :
for {
server.Listen(8080) // 服务器监听 8080 端口
}
数据结构和函数的使用
一、 结构体
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。 (相当于 Java 中的类 class )
go 语言中 结构体定义如下:
type identifier struct {
field1 type1
field2 type2
...
}
用结构体可以表示一个具有某一特征的对象。 field1, field2 ... 是对象的属性, type1, type2 是属性的数据类型。
按照面向对象的思想, “人” 、“书本”、“学校”、“电脑”、“空气” 等自然界的万事万物都可以用对象来表示。拿 “人” 这个对象来说,可以如下表示 :
type Person struct {
name string // 姓名
age uint32 // 年龄
birth string // 出生日期 用 yyyy/mm/dd 格式的字符串表示
height float32 // 身高
weight float32 // 体重
...
}
name, age, birth, height, weight 是 “人” 这个对象的姓名、年龄、出生日期、身高、体重等属性
string, uint32, float32 是 这些属性的数据类型
对结构体的赋值有两种方式 :
第一种 :
t := new(T) : 变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值,例如 :
person := new(Person)
第二种 :
t := &T{}
&T{} 这种语法叫做 混合字面量语法(composite literal syntax), 类似 &struct1{a, b, c} 这种混合字面量语法是一种简写,底层仍然会调用 new (),这里值的顺序必须按照字段顺序来写。
person := &Person {name : "lubanproj", age : 12}
如果要访问结构体成员,需要使用点号 . 操作符,格式为:
结构体.成员名"
例如 :
person := &Person {name : "lubanproj", age : 12}
fmt.Printf("name : %s, age : %d", person.name, person.age)
二、数组
数组是有序的元素序列。
go 中数组语法如下 :
var variable_name [SIZE] variable_type
例如 :
// 定义一个 10个长度 string 类型的 数组 array
var array [10] string
// 数组赋值
array[0] = "go"
array[1] = "java"
array[2] = "c++"
数组赋值
上面代码可以看出。直接通过访问数组下标即可对数组元素进行赋值。
数组初始化
声明时初始化,语法如下:
var variable_name = [SIZE]variable_type {value1, value2 ...}
例如 :
var array = [5]string{ "go", "java", "c++" }
数组遍历
普通 for 循环遍历,例如 :
for i:=0; i < 5; i++ {
fmt.Printf("array[%d] = %s n" , i, array[i])
}
for range 遍历,这是 go 里面一种特有的遍历方式,例如 :
// 这段代码可以实现跟上面代码相同效果
for index, value := range array {
fmt.Printf("array[%d] = %s n", index, value)
}
上面代码中,index 即为数组元素下标,value 即为数组元素的值,假如不需要使用到 index 或者 value ,可以用 _ 进行代替。
例如 :
// 这段代码可以实现跟上面代码相同效果
for _, value := range array {
fmt.Printf("value = %s n", value)
}
三、切片
go 提供了一种类似 “ 动态数组 ” 结构的数据类型,这种类型就是切片 slice。
slice 的本质是一个数据结构,实现了对数组操作的封装。
切片 slice 的声明语法如下 :
var identifier []type
例如 :
// 声明一个为 int64 类型的切片
var array []int64
你会发现 slice 和数组的声明语法是不同的。go 语言中声明数组时,是需要指定长度的, 例如 :
// 声明一个为 int64 类型、长度为 10 的数组
var array [10] int64
初始化操作也不一样,对 slice 的初始化是使用 make 初始化
// 初始化一个 int64 类型的切片
array = make ([]int64 , 10)
而对数组的初始化是:
array = [10] int64 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
切片slice 的常用方法
append : 实现对 slice 元素的添加 例如 :
array := make ( []int64 , 10 )
// 往数组中添加两个元素
array = append ( array, 1 )
array = append ( array, 2 )
截取 : 可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],例如:
package main
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])
/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:])
}
func printSlice(x []int){
fmt.Printf("len=%d slice=%v n" , len(x) , x)
}
执行以上代码输出结果为:
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
获取切片长度 : 通过 len 方法,可以获取 slice 长度
len ( slice )
遍历 : 和数组一样,slice 可以通过 range 关键字进行遍历,例如 :
package main
import "fmt"
func main() {
//使用range去求一个slice的和
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
}
四、map
map 是一种无序的键值对的集合
go 语言中 语法如下:
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
例如 :
var dataMap map[string]string
map 初始化
map 可以使用 make 进行初始化
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
例如 :
dataMap := make(map[string]string)
如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对
例如下面代码,会执行失败:
package main
func main() {
var dataMap map[string]string
dataMap["lubanproj"] = "hello lubanproj"
}
输出 :
panic: assignment to entry in nil map
所以 对于 map 类型,一定要进行初始化再赋值
和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。但是你也可以选择标明 map 的初始容量 capacity,语法如下:
make(map[keytype]valuetype, capacity)
例如:
dataMap := make(map[string]string, 20)
map 遍历
和 slice 相似, map 的遍历同样可以使用 for range 进行遍历,例如:
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %sn", k, v)
}
map 基本操作
假设有一个 map m ,下面演示下对 m 的操作
设置/修改 元素值
m[key] = elem
获取元素:
elem = m[key]
删除元素:
delete(m, key)
检测某个键是否存在:
elem, ok = m[key]
okfalse
同样的,当从 map 中读取某个不存在的键时,结果是 map 的元素类型的零值。
下面代码演示了对 map 的基本操作 :
package main
import "fmt"
func main() {
dataMap := make(map[string]string, 10)
dataMap["hunan"] = "changsha"
dataMap["hubei"] = "wuhan"
dataMap["guangdong"] = "guangzhou"
dataMap["guangxi"] = "guilin"
dataMap["anhui"] = "hefei"
// 遍历输出省和省会城市
for province, city := range dataMap {
fmt.Printf("%s 的省会是 %s n", province, city)
}
// 获取湖南省的省会
hunanCity := dataMap["hunan"]
beijingCity, ok := dataMap["heibei"]
fmt.Printf("hunan 省的省会城市是 : %s n",hunanCity)
if ok {
fmt.Printf("hebei 省的省会城市是 : %s n",beijingCity)
} else {
fmt.Printf("hebei 省的省会城市不存在 n")
}
delete(dataMap, "hunan")
hunanCity = dataMap["hunan"]
fmt.Printf("hunan 省的省会城市是 : %s n",hunanCity)
}
输出结果为 :
guangxi 的省会是 guilin
hubei 的省会是 wuhan
guangdong 的省会是 guangzhou
anhui 的省会是 hefei
hunan 的省会是 changsha
hunan 省的省会城市是 : changsha
hebei 省的省会城市不存在
hunan 省的省会城市是 :
五、函数
相信学过编程的同学都知道,函数是最基本的代码块
go 语言函数定义格式如下:
func function_name( [parameter list] ) [return_types] {
函数体
}
例如 :
// 此函数实现了取两个数最大值的功能
func max(a int32, b int32) int32 {
if a > b {
return a
} else {
return b
}
}
与许多后端语言不同的是, go 语言中的函数是支持多返回值的,比如我们将上述函数修改一下,实现取两个数最大值和最小值的功能。
// 此函数实现了取两个数最大值和最小值的功能
func getMaxAndMin(a int32, b int32) (int32, int32) {
if a > b {
return a, b
} else {
return b, a
}
}
方法
与其他语言不同的是,go 语言还有一种特殊的函数,叫做方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
例如 :
// 我们用 面向对象的思想实现一个封装的结构体
type Person struct{
name string // 姓名
age uint32 // 年龄
}
// 获取姓名
func (p *Person) GetName() string{
return p.name
}
// 获取年龄
func (p *Person) GetAge() uint32{
return p.age
}
// 设置姓名
func (p *Person) SetName(name string) {
p.name = name
}
// 设置年龄
func (p *Person) SetAge(age uint32) {
p.age = age
}
异常处理机制和面向对象
身为一门健壮、高效的语言,go 提供了异常处理机制来实现容错性,以及提供了类似 java 的面向对象的能力来实现高内聚、低耦合。
这个章节我们分别介绍下异常处理机制和面向对象是如何实现的。
一、异常处理
众所周知,很多语言(例如:java、c++ 等)都有 exception 的 try — catch — finally 异常捕获机制 ,go 语言中一般不采用这种异常捕获机制,而是通过下面几种方式进行异常处理。
error 接口
go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error 类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
使用 errors.New () 可以返回一个 error 信息 例如 :
func checkParam(username string, password string) error {
if username == "" || password == "" {
return errors.New("params error")
}
}
一般来说, error 是 go 语言中最常见的处理错误的方式,通过返回 error,处理 error, 产生类似 exception 的效果。
defer 语句
在 go 语言中,可以使用关键字 defer 向函数注册退出调用,即主调函数退出时,defer后的函数才会被调用。
defer 语句的作用是不管程序是否出现异常,均在函数退出时自动执行相关代码。(相当于 finally 代码块)
例如 :
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
其执行结果为 :
4
3
2
1
0
所以,我们在进行数据库连接、文件、锁 操作时 ,一般都会使用 defer 语句进行数据库连接的释放,释放文件句柄和锁的释放。
panic-recover 机制
我们在上文说到可以在方法中抛出 error 错误来实现异常的捕获,但是 error 只能针对预期内的错误,因为你是预判这段程序可能出现异常逻辑,才会去主动调用 errors.New () 生成一个 error 。但是对于一个方法来说,我们不可能预判到所有的异常情况,那假如某一个隐藏 bug 导致程序崩溃了怎么办呢?这里就需要引入 panic-recover 机制了。
假如代码运行时异常崩溃了,此时 go 会自动 panic,go 的每次 panic 都是非常消耗性能的,且 go 是单线程,所以,我们应该尽量去避免使用 panic。
panic () 是一个内建函数,可以中断原有的控制流程,进入一个令人 panic (恐慌 ,即 Java 中的异常)的流程中。当函数 F 调用 panic,函数 F的执行被中断,但是 F 中的延迟函数(必须是在 panic 之前的已加载的 defer )会正常执行,然后 F 返回到调用它的地方。在调用的地方,F 的行为就像调用了panic。这一过程继续向上,直到发生 panic 的 goroutine 中所有调用的函数返回,此时程序退出。异常可以直接调用 panic 产生。也可以由运行时错误产生,例如访问越界的数组。
recover () 是一个内建的函数,可以让进入令人恐慌的流程中的 goroutine 恢复过来。recover 仅在延迟函数中有效。在正常的执行过程中,调用 recover 会返回 nil ,并且没有其它任何效果。如果当前 goroutine 陷入 panic ,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。
一般情况下,recover () 应该在一个使用 defer 关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的 goroutine 中明确调用恢复过程(使用 recover 关键字),会导致该 goroutine 所属的进程打印异常信息后直接退出。
这里结合自定义的 error 类型给出一个使用 panic 和 recover 的完整例子:
package main
import (
"fmt"
)
//定义除法运算函数
func Devide(num1, num2 int) int {
if num2 == 0 {
panic("num cannot be 0")
} else {
return num1 / num2
}
}
func main() {
var a, b int
fmt.Scanf("%d %d", &a, &b)
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic的内容%vn", r)
}
}()
rs := Devide(a, b)
fmt.Println("结果是:", rs)
}
二、面向对象
面向对象是程序设计的一种基本思想。面向对象思想主要体现在封装、继承、多态等特性的设计与运用。那么在 go 中是如何去进行实现的呢?
封装
封装主要是通过访问权限控制实现的。 以 Java 为例,共有 public 、protected、default、private 四种权限,每个关键字的权限范围如下:
在 go 语言中,并没有 public ,private 这些权限控制符。那么 go 是如何实现 结构体的封装的呢 ?
在 go 语言中,是通过约定来实现权限控制的。变量和方法都遵守驼峰式命名。变量和方法的首字母大写,相当于 public,变量和方法的首字母小写,相当于 private。同一个包中访问,相当于 default ,由于 go 语言没有继承,所以也没有 protected 权限。
继承
上面刚说到,go 语言是没有继承的。但是 go 语言可以通过结构体之间的组合来实现类似继承的效果。
假如把 go 中 struct 看做 Java 或者 c++ 中的类 class ,在 struct 中可以包含其他的struct,继承内部 struct 的方法和变量,同时可以重写,代码如下:
package main
import "fmt"
type oo struct {
inner
ss1 string
ss2 int
ss3 bool
}
type inner struct {
ss4 string
}
func (i *inner) testMethod () {
fmt.Println("testMethod is called!!!")
}
func main() {
oo1 := new(oo)
fmt.Println("ss4无值:"+oo1.ss4)
oo1.ss4 = "abc"
fmt.Println("ss4已赋值"+oo1.ss4)
oo1.testMethod()//继承调用
oo1.inner.testMethod()//继承调用 这里也可以重写
}
多态
Java 中的多态是通过 extends class 或者 implements interface 实现的,在 go 中既没有 extends,也没有 implements ,那么 go 中是如何实现多态的呢 ?
我们来看以下代码,Girl 和 Boy 都实现了 Person 。 在 go 语言中,只要某个 struct 实现了某个 interface 的所有方法,那么我们就认为这个 struct 实现了这个类(相当于 Java 中的implements)。
package main
import (
"fmt"
)
type Person interface {
Sing ()
}
type Girl struct {
Name string
}
type Boy struct {
Name string
}
func (this *Girl) Sing () {
fmt.Println("Hi, I am " + this.Name)
}
func (this *Boy) Sing () {
fmt.Println("Hi, I am " + this.Name)
}
func main() {
g := &Girl{"Lucy"}
b := &Boy{"Dave"}
p := map[int]Person{}
p[0] = g
p[1] = b
for _, v := range p {
v.Sing()
}
}