非零基础速成Go语言_基础篇
这里不带着安装 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 - else 条件语句
- switch - case 选择语句
- for - range 循环语句
- goto 无条件跳转语句
- defer 延迟执行
条件模型
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 函数的。