这里不带着安装 Go 环境了,推荐大家准备 Goland + Go 环境
以下是所有语言都有的特性,看看 Go 有什么不一样的地方
Go 语言中,整数类型可以再细分成10个类型
int 代表有符号,可表示负数 uint 代表无符号,只能表示正数。
数组定义
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。一般用切片代替,即 Java 中的 List 结构
// [3] 里的3 表示该数组的元素个数及容量 var arr [3]int arr[0] = 1 arr[1] = 2 arr[2] = 3
定义并使用
// 第一种方法 var arr [3]int = [3]int{1,2,3} // 第二种方法 arr := [3]int{1,2,3}
切片(Slice)与数组一样,也是可以容纳若干类型相同的元素的容器。与数组不同的是,无法通过切片类型来确定其值的长度。每个切片值都会将数组作为其底层数据结构。我们也把这样的数组称为切片的底层数组
当你在初始化时不制定容量,它就是容量动态变化的数组
切片追加元素的函数是 append 方法:
func main() { //初始化一个切片 myarr := []int{1} // 追加一个元素 myarr = append(myarr, 2) // 追加多个元素 myarr = append(myarr, 3, 4) // 追加一个切片, ... 表示解包,不能省略 myarr = append(myarr, []int{7, 8}...) // 在第一个位置插入元素 myarr = append([]int{0}, myarr...) // 在中间插入一个切片(两个元素) myarr = append(myarr[:5], append([]int{5,6}, myarr[5:]...)...)
类似 Java 的 HashMap,用来存储键值对元素。
与 HashMap 一样,要求 key 是唯一的,以便支持 Hash 特性
在声明字典时,必须指定好你的key和value是什么类型的,然后使用 map 关键字来告诉Go这是一个字典
map[KEY_TYPE]VALUE_TYPE
三种声明并初始化字典的方法
// 第一种方法 var scores map[string]int = map[string]int{"english": 80, "chinese": 85} // 第二种方法 scores := map[string]int{"english": 80, "chinese": 85} // 第三种方法 //make 代表为 map 开辟内存空间 score 指向这块空间 scores := make(map[string]int) scores["english"] = 80 scores["chinese"] = 85
要注意的是,第一种方法如果拆分成多步(声明、初始化、再赋值),和其他两种有很大的不一样了,相对会比较麻烦。
// 声明一个名为 score 的字典 var scores map[string]int // 未初始化的 score 的零值为nil,否则无法直接进行赋值 if scores == nil { // 需要使用 make 函数先对其初始化 scores = make(map[string]int) } // 经过初始化后,就可以直接赋值 scores["chinese"] = 90 fmt.Println(scores) }
循环获取 map 的减值
scores := map[string]int{"english": 80, "chinese": 85} //键和值都要 for subject, score := range scores { fmt.Printf("key: %s, value: %d\n", subject, score) } //只要值 for _, score := range scores { fmt.Printf("key: %s, value: %d\n", subject, score) } //只要键 for subject, _ := range scores { fmt.Printf("key: %s, value: %d\n", subject, score) }
在 Java中 基本数据类型传递为值传递 引用类型传递就地址传递,在 Go 中也是类似的,但是操作起来还是有些许区别:Go 中指针/引用是在 C++上进行的改良
当我们定义一个变量 name
var name string = "Go"
此时,name 是变量名,它只是编程语言中方便程序员编写和理解代码的一个标签。
当我们访问这个标签时,会返回给我们它指向的内存地址里存储的值:“Go”。
出于某些需要,我们会将这个内存地址赋值给另一个变量名,通常叫做 ptr(pointer的简写),而这个变量,我们称之为指针变量。
换句话说,指针变量(一个标签)的值是指针,也就是内存地址,与 Java 中的引用变量一样
根据变量指向的值,是内存地址还是内存里的值,我把变量分为两种:
创建指针
指针创建有三种方法
1.先定义对应的变量,再通过变量取得内存地址,创建指针
// // 定义普通变量 aint := 1 // 定义指针变量 ptr := &aint
2.先创建指针,分配好内存后,再给指针指向的内存地址写入对应的值。
// 创建指针 astr := new(string) // 给指针赋值 *astr = "Go"
3.先声明一个指针变量,再从其他变量取得内存地址赋值给它
aint := 1 var bint *int // 声明一个指针 bint = &aint // 初始化
上面的三段代码中,指针的操作都离不开这两个符号:
&
:从一个普通变量中取得内存地址*
:当*
在赋值操作符(=)的右边,是从一个指针变量中取得变量的值,当*
在赋值操作符(=)的左边,是指该指针指向的变量Go里的流程控制方法还是挺丰富,整理了下有如下这么多种:
条件模型
if 条件 1 { 分支 1 } else if 条件 2 { 分支 2 } else if 条件 ... { 分支 ... } else { 分支 else }
这部分不用多说,会任意一门语言的都行
这里需要注意,go 的 if 后允许接收表达式 对返回的值进行判断 这是一个比较常用的写法
if age := 20;age > 18 { fmt.Println("已经成年了") }
Swith-case
与其他语言一样
switch 表达式 { case 表达式1: 代码块 case 表达式2: 代码块 case 表达式3: 代码块 case 表达式4: 代码块 case 表达式5: 代码块 default: 代码块 }
for
go 的循环只有 for 一种
for [condition | ( init; condition; increment ) | Range] { statement(s); }
基于其模型,有 while 的等效 写法
a := 1 for a <= 5 { fmt.Println(a) a ++ }
常规 for 写法
for i := 1; i <= 5; i++ { fmt.Println(i) }
无限循环写法
for { 代码块 } // 等价于 for ;; { 代码块 }
【常用】集合遍历写法 for-range
遍历一个可迭代对象,是一个很常用的操作。在 Go 可以使用 for-range 的方式来实现
range 后可接数组、切片,字符串等
由于 range 会返回两个值:索引和数据,若你后面的代码用不到索引,需要使用 _
表示
myarr := [...]string{"world", "python", "go"} for _, item := range myarr { fmt.Printf("hello, %s\n", item) }
注意:如果你用一个变量来接收的话,接收到的是索引
go to 无条件跳转
goto
可以打破原有代码执行顺序,直接跳转到某一行执行代码
goto flag fmt.Println("B") flag: fmt.Println("A")
执行结果,并不会输出 B ,而只会输出 A
goto
语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能,类似 Java 的 break 和 continue 关键字
i := 1 for { if i > 5 { goto flag } fmt.Println(i) i++ } flag:
用 goto 实现 类型 continue的效果
i := 1 flag: for i <= 10 { if i%2 == 1 { i++ goto flag } fmt.Println(i) i++ }
注意:goto语句与标签之间不能有变量声明,否则编译错误。
defer 延迟语句
这是 Go 独有的特性,它允许函数执行完毕后 执行 defer 再跳转,如我们的程序在函数中使用一些资源,最后关闭的时候可以使用 defer,比如归还数据库连接资源等
func myfunc() { fmt.Println("B") } func main() { defer myfunc() fmt.Println("A") }
使用 defer 只是延时调用函数,此时传递给函数里的变量,不应该受到后续程序的影响。
比如这边的例子
import "fmt" func main() { name := "go" defer fmt.Println(name) // 输出: go name = "python" fmt.Println(name) // 输出: python }
输出如下,可见给 name 重新赋值为 python
,后续调用 defer 的时候,仍然使用未重新赋值的变量值,就好在 defer 这里,给所有的这是做了一个快照一样。
python go
如果 defer 后面跟的是匿名函数,情况会有所不同, defer 会取到最后的变量值
package main import "fmt" func main() { name := "go" defer func(){ fmt.Println(name) // 输出: python }() name = "python" fmt.Println(name) // 输出: python }
注意: defer 是return 后才调用的
编程语言一般都会有异常捕获机制,在 Java 中 是使用throw / catch 和 try
语句来实现的异常抛出和异常捕获的
在 Golang 中,有不少常规错误,在编译阶段就能提前告警,比如语法错误或类型错误等,但是有些错误仅能在程序运行后才能发生,比如数组访问越界、空指针引用等,这些运行时错误会引起程序退出。
当然能触发程序宕机退出的,也可以是我们自己,比如经过检查判断,当前环境无法达到我们程序进行的预期条件时(比如一个服务指定监听端口被其他程序占用),可以手动触发 panic,让程序退出停止运行。
手动触发宕机,是非常简单的一件事,只需要调用 panic 这个内置函数即可,就像这样子
func main() { panic("crash") }
发生了异常,有时候就得捕获
这就不得不引出另外一个内建函数 – recover
,它可以让程序在发生宕机后起生回生。
但是 recover 的使用,有一个条件,就是它必须在 defer 函数中才能生效,其他作用域下,它是不工作的。
这是一个简单的例子:
import "fmt" func set_data(x int) { defer func() { // recover() 可以将捕获到的panic信息打印 if err := recover(); err != nil { fmt.Println(err) } }() // 故意制造数组越界,触发 panic var arr [10]int arr[x] = 88 } func main() { set_data(20) // 如果能执行到这句,说明panic被捕获了 // 后续的程序能继续运行 fmt.Println("everything is ok") }
注意:无法跨协程:即使 panic 会导致整个程序退出,但在退出前,若有 defer 延迟函数,还是得执行完 defer 。
但是这个 defer 在多个协程之间是没有效果,在子协程里触发 panic,只能触发自己协程内的 defer,而不能调用 main 协程里的 defer 函数的。