go基础语法入门
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。
Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。
Go 语言特色
- 简洁、快速、安全
- 并行、有趣、开源
- 内存管理、数组安全、编译迅速
计算机软件经历了数十年的发展,形成了多种学术流派,有面向过程编程、面向对象编程、函数式编程、面向消息编程等,这些思想究竟孰优孰劣,众说纷纭。
除了OOP外,近年出现了一些小众的编程哲学,Go语言对这些思想亦有所吸收。例如,Go语言接受了函数式编程的一些想法,支持匿名函数与闭包。再如,Go语言接受了以Erlang语言为代表的面向消息编程思想,支持goroutine和通道,并推荐使用消息而不是共享内存来进行并发编程。总体来说,Go语言是一个非常现代化的语言,精小但非常强大。
Go 语言最主要的特性:
- 自动垃圾回收
- 更丰富的内置类型
- 函数多返回值
- 错误处理
- 匿名函数和闭包
- 类型和接口
- 并发编程
- 反射
- 语言交互性
编译速度:
- Go 语言使用了更加智能的编译器,并简化了解决依赖的算法,最终提供了更快的编译速度。
- 编译 Go 程序时,编译器只会关注那些直接被引用的库,而不是像 Java、 C 和 C++那样,要遍历依赖链中所有依赖的库。因此,很多 Go 程序可以在 1 秒内编译完。
Go 语言用途
Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。
对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。
获取go代码
go get 命令可以借助代码管理工具通过远程拉取或更新代码包及其依赖包,并自动完成编译和安装。
go get 使用时的附加参数
- -v 显示操作流程的日志及信息,方便检查错误
- -u 下载丢失的包,但不会更新已经存在的包
- -d 只下载,不安装
- -insecure 允许使用不安全的 HTTP 方式进行下载操作
远程包的路径格式 : github.com/golang/go
- 网站域名:表示代码托管的网
- 作者或机构:表明这个项目的归属,一般为网站的用户名,如果需要找到这个作者下的所有项目,可以直接在网站上通过搜索“域名/作者”进行查看
- 项目名:每个网站下的作者或机构可能会同时拥有很多的项目
//获取前,请确保 GOPATH 已经设置。Go 1.8 版本之后,GOPATH 默认在用户目录的 go 文件夹下
go get github.com/golang/go
第一个 Go 程序
hello.go
package main
import "fmt"
func main(){
fmt.Println("Hello World!")
}
要执行 Go 语言代码可以使用 go run 命令。
go run hello.go
此外我们还可以使用 go build 命令来生成二进制文件:
go build hello.go
./hello
Go 基础语法
go源码文件:.go为后缀,内容以go语言代码组织的文件
代码包的作用: 编译和归档go程序的最基本单位;代码划分、集结和依赖的有效组织形式,也是权限控制的辅助手段;
go语言结构
- 包声明
- 引入包
- 函数
- 变量
- 语句 & 表达式
- 注释
- 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,
- 那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);
- 标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )
package main //表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包
import "fmt" //fmt 包实现了格式化 IO(输入/输出)的函数
//main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)
func main() { //需要注意的是 { 不能单独放在一行
/* 这是我的第一个简单的程序 */
fmt.Println("Hello, World!")
}
//一个可执行程序有且仅有一个 main 包
//通过 import 关键字来导入其他非 main 包
// 为fmt起别名为fmt2
import fmt2 "fmt"
关于包,根据本地测试得出以下几点:
- 文件名与包名没有直接关系,不一定要将文件名与包名定成同一个。
- 文件夹名与包名没有直接关系,并非需要一致。
- 同一个文件夹下的文件只能有一个包名,否则编译报错。
执行go程序
go run hello.go 运行go程序
go build hello.go 生成二进制文件
当前的调试部分可以使用 go run filename.go 来执行
行分隔符
在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。
如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。
注释
注释不会被编译,每一个包应该有相关注释。
单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾。
go源码文件分类:
- 命令源码文件(声明自己属于main代码包、包含无参数声明和结果声明的main函数);同一个代码包中强烈不建议直接包含多个命令源码文件
- 库源码文件(不具备命令源码文件两个特征的源码文件)
- 测试源码文件(不具备命令源码文件两个特征的源码文件,名称以_test.go为后缀);测试函数以Test(功能测试函数)或Benchmark为前缀(性能测试函数)
程序实体与关键字:
- 在go语言中,变量、常量、函数、结构体和接口被统称为“程序实体”,而它们的名字被统称为“标识符”。
- 标识符可以是任何Unicode编码可以表示的字母、数字及下划线。首字母不能是数字或下划线。
- 对程序实体的访问权限控制只能通过它们的名字来实现。名字首字母大写的程序实体可以被任何代码包访问到。而名字首字母小写的程序实体则只能被同一个代码包中的代码所访问。
- 关键字不能作为标识符。
变量和常量:
- 声明变量的关键字var,声明常量的关键字const
- 常量只能被赋予基本数据类型的值本身,常量不能只声明不赋值
var num1 int = 1 // 普通赋值: var、变量名称、变量类型、=、变量值
var num2,num3 int = 2,3 //平行赋值
// 多行赋值
var (
num4 int = 4
num5 int = 5
)
//这种不带声明格式的只能在函数体中出现
//g, h := 123, "hello"
值类型和引用类型
- 所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值
- 当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝
- 你可以通过 &i 来获取变量 i 的内存地址,例如:0xf840000040(每次的地址都可能不一样)。值类型的变量的值存储在栈中。
- 更复杂的数据通常会需要使用多个字节,这些数据一般使用引用类型保存
- 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置
- 这个内存地址为称之为指针,这个指针实际上也被存在另外的某一个字中。
- 同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;
- 也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。
- 当使用赋值语句 r2 = r1 时,只有引用(地址)被复制。
- 如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容
使用 := 赋值操作符
- 可以在变量的初始化时省略变量的类型而由系统自动推断,声明语句写上 var 关键字其实是显得有些多余了,因此我们可以将它们简写为 a := 50 或 b := false。
- a 和 b 的类型(int 和 bool)将由编译器自动推断。
- 这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。
- 使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明。
如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a,两个变量的类型必须是相同。
空白标识符
- _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。
- _ 实际上是一个只写变量,你不能得到它的值。
- 这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。
并行赋值也被用于当一个函数返回多个返回值时
- 比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到:val, err = Func1(var1)
常量:
- 常量是一个简单值的标识符,在程序运行时,不会被修改的量。
iota
- iota,特殊常量,可以认为是一个可以被编译器修改的常量。
- iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
- iota 可以被用作枚举值
//第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
const (
a = iota
b
c
)
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
字符串
字符串就是一串固定长度的字符连接起来的字符序列
字符串连接:Go 语言的字符串可以通过 + 实现:
fmt.Println("Google" + "Runoob")
浮点数类型:
- 浮点数类型有两个,即float32和float64。存储这两个类型的值的空间分别需要4个字节和8个字节。
- 浮点数类型的值一般由整数部分、小数点“.”和小数部分组成。
- 指数部分由“E”或“e”以及一个带正负号的10进制数组成。比如,3.7E-2表示浮点数0.037。又比如,3.7E+1表示浮点数37
- 有一点需要注意,在Go语言里,浮点数的相关部分只能由10进制表示法表示,而不能由8进制表示法或16进制表示法表示。
Go 语言运算符
运算符用于在程序运行时执行数学或逻辑运算。
Go 语言内置的运算符有:
- 算术运算符
- 关系运算符 ==, !=, >, <
- 逻辑运算符 &&, ||, !
- 位运算符 &, |, ^, <<, >>
- 赋值运算符 =
- 其他运算符 &-返回变量存储地址, *-指针变量
优先级 | 运算符 |
---|---|
5 | * / % << >> & &^ |
4 | + - |
3 | == != < <= > >= |
2 | && |
1 | || |
语言类型转换
类型转换用于将一种数据类型的变量转换为另外一种类型的变量
//Go 语言类型转换基本格式
type_name(expression)
func main() {
var sum int = 17
var count int = 5
var mean float32
mean = float32(sum)/float32(count)
fmt.Printf("mean 的值为: %f\n",mean)
}
数组(Array):
就是一个可以容纳若干类型相同的元素的容器。这个容器的大小(即数组的长度)是固定的,且是体现在数组的类型字面量之中的。
type MyNumbers [3]int //类型声明语句由:关键字type、类型名称、类型字面量组成。
var numbers = [3]int{1, 2, 3} //变量声明语句,声明变量的同时为该变量赋值。
var numbers = [...]int{1, 2, 3} //类型字面量中省略代表其长度的数字
numbers[0] //得到第一个元素
var length = len(numbers) //len是Go语言的内建函数的名称。该函数用于获取字符串、数组、切片、字典或通道类型的值的长度。
var numbers2 [5]int //只声明一个数组类型的变量,那么该变量的值将会是指定长度的、其中各元素均为元素类型的零值。[5]int{0, 0, 0, 0, 0}
指针(pointer)
变量是一种使用方便的占位符,用于引用计算机内存地址,取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
一个指针变量指向了一个值的内存地址。
在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。
当一个指针被定义后没有分配到任何变量时,它的值为 nil。nil 指针也称为空指针。
指针使用流程:
- 定义指针变量。
- 为指针变量赋值。
- 访问指针变量中指向地址的值。
//指针声明格式如下:var var_name *var-type
var ip *int /* 指向整型*/
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
//指针作为函数参数
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址的值 */
*x = *y /* 将 y 赋值给 x */
*y = temp /* 将 temp 赋值给 y */
}
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200
fmt.Printf("交换前 a 的值 : %d\n", a )
fmt.Printf("交换前 b 的值 : %d\n", b )
/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &b 指向 b 变量的地址
*/
swap(&a, &b);
fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}
调用函数,可以通过两种方式来传递参数:
- 值传递 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
- 引用传递 引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
切片类型(Slice):
是可以容纳若干类型相同的元素的容器。与数组不同的是,无法通过切片类型来确定其值的长度。
- 作为切片表达式求值结果的切片值的长度总是为元素上界索引与元素下界索引的差值。
- 一个切片值的容量即为它的第一个元素值在其底层数组中的索引值与该数组长度的差值的绝对值。
- 切片类型属于引用类型,它的零值即为nil
- 切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
[]int //表示切片类型的字面量
type MySlice []int //切片类型的声明,MySlice即为切片类型[]int的一个别名类型
[]int{1, 2, 3} //对切片值的表示也与数组值也极其相似
//实施切片操作的方式就是切片表达式
var numbers3 = [5]int{1, 2, 3, 4, 5}
var slice1 = numbers3[1:4] //切片表达式numbers3[1:4]的求值结果为[]int{2, 3, 4},slice1这个切片值的底层数组正是numbers3的值
var slice2 = slice1[1:3] //在一个切片值上实施切片操作 slice2的值为[]int{3, 4}
var capacity2 int = cap(slice2) //获取数组、切片或通道类型的值的容量,内建函数cap
//使用make()函数来创建切片
slice1 := make([]type, len)
//可以指定容量,其中capacity为可选参数
make([]T, length, capacity)
//切片初始化
s :=[] int {1,2,3 }
//初始化切片s,将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:endIndex]
//默认 endIndex 时将表示一直到arr的最后一个元素
s := arr[startIndex:]
//默认 startIndex 时将表示从arr的第一个元素开始
s := arr[:endIndex]
//通过切片s初始化切片s1
s1 := s[startIndex:endIndex]
//append() 和 copy() 函数
//如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
func main() {
var numbers []int
printSlice(numbers)
/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers)
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers)
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
字典类型(Map):
是哈希表(Hash Table)的一个实现。字典用于存储键-元素对(更通俗的说法是键-值对)的无序集合。
- 注意,同一个字典中的每个键都是唯一的
- 字典类型属于引用类型,它的零值即为nil
map[K]T //字典类型的字面量:K-键的类型,T-元素的类型
map[int]string //字典的键类型必须是可比较的,否则会引起错误
map[int]string{1: "a", 2: "b", 3: "c"} //map[int]string,它的值的字面量
mm := map[int]string{1: "a", 2: "b", 3: "c"}
b := mm[2] //运用索引表达式取出字典中的值
e, ok := mm[5] //字典的索引表达式可以有两个求值结果。第二个求值结果是bool类型的,它用于表明字典值中是否存在指定的键值对。不存在键5,变量ok必为false
delete(mm, 4) //删除键值对,调用内建函数delete
通道类型(Channel):
是Go语言中一种非常独特的数据结构,它可用于在不同Goroutine之间传递类型化的数据,并且是并发安全的。
- Goroutine(也称为Go程序)可以被看做是承载可被并发执行的代码块的载体
- Goroutine 它们由Go语言的运行时系统调度,并依托操作系统线程(又称内核线程)来并发地执行其中的代码块
- 通道类型属于引用类型,它的零值即为nil
- 通道有带缓冲和非缓冲之分:缓冲通道中可以缓存N个数据,我们在初始化一个通道值的时候必须指定这个N
- 相对的,非缓冲通道不会缓存任何数据,发送方在向通道值发送数据的时候会立即被阻塞,直到有某一个接收方已从该通道值中接收了这条数据。
chan T //通道类型关键字chan,T 代表该通道类型允许传递的数据的类型
//无法用字面量来为通道类型的变量赋值。只能通过调用内建函数make来达到目的。make函数可接受两个参数。
make(chan int, 5) //第一个参数是代表:将被初始化的值的类型的字面量,而第二个参数则是值的长度。
ch1 := make(chan string, 5) //声明一个通道类型的变量,并为其赋值
ch1 <- "value1" //使用接收操作符<-向通道发送数据
value := <- ch1 //从ch1那里接收字符串
//为了消除与零值有关的歧义,变量ok的值是bool类型
//它代表了通道值的状态,true代表通道值有效,而false则代表通道值已无效(或称已关闭)
value, ok := <- ch1
close(ch1) //关闭通道值调用内建函数close, 通道值的重复关闭会引发运行时恐慌,这会使程序崩溃
make(chan int, 0) //非缓冲的通道值的初始化
//以数据在通道中的传输方向为依据来划分通道:默认情况下,通道都是双向的,即双向通道;
//如果数据只能在通道中单向传输,那么该通道就被称作单向通道
//单向通道的主要作用是约束程序对通道值的使用方式
//比如,我们调用一个函数时给予它一个发送通道作为参数,以此来约束它只能向该通道发送数据。
//类型Receiver代表了一个只可从中接收数据的单向通道类型,这样的通道也被称为接收通道
type Receiver <-chan int
// 声明一个发送通道类型
type Sender chan<- int
//通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y)
}
并发
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
//goroutine 语法格式:
go 函数名( 参数列表 )
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
函数(func)
- 在Go语言中,函数是一等(first-class)类型。这意味着,我们可以把函数作为值来传递和使用
- 函数代表着这样一个过程:它接受若干输入(参数),并经过一些步骤(语句)的执行之后再返回输出(结果)
- Go语言中的函数可以返回多个结果
- 函数类型的零值是nil
//函数类型的字面量由关键字func、由圆括号包裹参数声明列表、空格以及可以由圆括号包裹的结果声明列表组成。
func(input1 string ,input2 string) string
//函数类型声明
type MyFunc func(input1 string ,input2 string) string
//编写函数的时候需要先写关键字func和函数名称,后跟参数声明列表和结果声明列表,最后是由花括号包裹的语句列表
//如果结果声明是带名称的,那么它就相当于一个已被声明但未被显式赋值的变量
//可以为它赋值且在return语句中省略掉需要返回的结果值
//函数myFunc是函数类型MyFunc的一个实现
//只要一个函数的参数声明列表和结果声明列表中的数据类型的顺序和名称与某一个函数类型完全一致,前者就是后者的一个实现
func myFunc(part1 string, part2 string) (result string) {
result = part1 + part2
return
}
// 等价于 var splice MyFunc,然后把函数myFunc赋给它:splice = myFunc
var splice func(string, string) string
//调用动作
splice("1", "2")
//直接使用了一个匿名函数来初始化splice变量
var splice = func(part1 string, part2 string) string {
return part1 + part2
}
//闭包 闭包是匿名函数,可在动态编程中使用
//这里的result变量的类型不是函数类型,而与后面的匿名函数的结果类型是相同的
var result = func(part1 string, part2 string) string {
return part1 + part2
}("1", "2")
func main() {
_,numb,strs := numbers() //只获取函数返回值的后两个
fmt.Println(numb,strs)
}
//一个可以返回多个值的函数
func numbers()(int,int,string){
a , b , c := 1 , 2 , "str"
return a,b,c
}
函数是基本的代码块,用于执行一个任务。
Go 语言最少有个 main() 函数。
函数作为另外一个函数的实参 函数定义后可作为另外一个函数的实参数传入
结构体(struct)
可以封装属性和操作
- 结构体类型属于值类型。它的零值并不是nil,而是其中字段的值均为相应类型的零值的值
- 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合
//结构体类型的字面量由关键字type、类型名称、关键字struct,以及由花括号包裹的若干字段声明组成
//每个字段声明独占一行并由字段名称(可选)和字段类型组成
type Person struct {
Name string
Gender string
Age uint8
}
//一旦定义了结构体类型,它就能用于变量的声明,语法格式如下:
variable_name := structure_variable_type {value1, value2...valuen}
//或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
//用字面量创建出一个该类型的值
Person{Name: "Robert", Gender: "Male", Age: 33}
//键值对的顺序与其类型中的字段声明完全相同
Person{"Robert", "Male", 33}
//匿名结构体
//匿名结构体最大的用处就是在内部临时创建一个结构以封装数据,而不必正式为其声明相关规则
//而在涉及到对外的场景中,强烈建议使用正式的结构体类型
p := struct {
Name string
Gender string
Age uint8
}{"Robert", "Male", 33}
//访问结构体成员
//如果要访问结构体成员,需要使用点号 . 操作符,格式为: 结构体.成员名"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
}
//结构体指针
var struct_pointer *Books
//指针变量可以存储结构体变量的地址
struct_pointer = &Book1
//使用结构体指针访问结构体成员,使用 "." 操作符:
struct_pointer.title
//方法就是一个包含了接受者的函数
//结构体类型可以拥有若干方法;
//所谓方法,其实就是一种特殊的函数,它可以依附于某个自定义类型
//方法的特殊在于它的声明包含了一个接收者声明,这里的接收者指代它所依附的那个类型
//在关键字func和名称Grow之间的那个圆括号及其包含的内容就是接收者声明
func (person *Person) Grow() {
person.Age++
}
//接收者声明的内容由两部分组成:第一部分是代表它依附的那个类型的值的标识符,第二部分是它依附的那个类型的名称
//后者表明了依附关系,而前者则使得在该方法中的代码可以使用到该类型的值
//需要注意的是,在Grow方法的接收者声明中的那个类型是*Person,而不是Person。实际上,前者是后者的指针类型。
p := Person{"Robert", "Male", 33}
p.Grow()
接口(interface):
一个接口类型总是代表着某一种类型(即所有实现它的类型)的行为
- 如果一个数据类型所拥有的方法集合中包含了某一个接口类型中的所有方法声明的实现,那么就可以说这个数据类型实现了那个接口类型
//一个接口类型的声明通常会包含关键字type、类型名称、关键字interface以及由花括号包裹的若干方法声明
//接口类型中的方法声明是普通的方法声明的简化形式
//们只包括方法名称、参数声明列表和结果声明列表,其中的参数的名称和结果的名称都可以被省略
type Animal interface {
Grow()
Move(string) string
Eat(name string) (food string)
}
//空接口类型即是不包含任何方法声明的接口类型,常简称为空接口
//Go语言中的包含预定义的任何数据类型都可以被看做是空接口的实现
interface{}
//判定*Person类型实现了Animal接口,在Go语言中,可以用类型断言来实现
//不能在一个非接口类型的值上应用类型断言来判定它是否属于某一个接口类型的。必须先把前者转换成空接口类型的值。
//*Person类型转换成空接口类型的值
p := Person{"Robert", "Male", 33, "Beijing"}
//在类型字面量后跟由圆括号包裹的值就构成了一个类型转换表达式,意为将后者转换为前者类型的值
v := interface{}(&p)
//在v上应用类型断言
//类型断言表达式的求值结果可以有两个:第一个结果是被转换后的那个目标类型(这里是Animal)的值,而第二个结果则是转换操作成功与否的标志
h, ok := v.(Animal)
条件语句(if)
- Go语言的流程控制主要包括条件分支、循环和并发,条件表达式是指其结果类型是bool的表达式
//if语句一般会由关键字if、条件表达式和由花括号包裹的代码块组成
//变量赋值直接加入到if子句中,被叫做if语句的初始化子句;它应被放置在if关键字和条件表达式之间,并与前者由空格分隔
//标识符的重声明:if语句内部对number的访问和赋值都只会涉及到第二次声明的那个number变量,这种现象也被叫做标识符的遮蔽。
var number int
if number := 4; 100 > number {
number += 3
}
多分支条件语句(switch)
- case代表分支,每一个case可以携带一个表达式或一个类型说明符
- case表达式的结果类型需要与switch表达式的结果类型一致
var name string
switch name {
case "Golang":
fmt.Println("A programming language from Google.")
case "Rust":
fmt.Println("A programming language from Mozilla.")
default:
fmt.Println("Unknown!")
}
//类型switch语句:紧随case关键字的不是表达式,而是类型说明符
//类型说明符由若干个类型字面量组成,且多个类型字面量之间由英文逗号分隔
//switch表达式是非常特殊的,这种特殊的表达式也起到了类型断言的作用,但其表现形式很特殊,如:v.(type)
//v必须代表一个接口类型的值,该类表达式只能出现在类型switch语句中,且只能充当switch表达式
v := 11
switch i := interface{}(v).(type) {
case int, int8, int16, int32, int64:
fmt.Printf("A signed integer: %d. The type is %T. \n", i, i)
case uint, uint8, uint16, uint32, uint64:
fmt.Printf("A unsigned integer: %d. The type is %T. \n", i, i)
default:
fmt.Println("Unknown!")
}
//fallthrough 它既是一个关键字,又可以代表一条语句
//fallthrough 语句可被包含在表达式switch语句中的case语句中,它的作用是使控制权流转到下一个case
//fallthrough 语句仅能作为case语句中的最后一条语句出现;并且,包含它的case语句不能是其所属switch语句的最后一条case语句
语言范围(Range)
range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素
在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对
func main() {
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}
循环语句(for)
//for语句代表着循环。一条语句通常由关键字for、初始化子句、条件表达式、后置子句和以花括号包裹的代码块组成
for i := 0; i < 10; i++ {
fmt.Print(i, " ")
}
//range子句包含一个或两个迭代变量(用于与迭代出的值绑定)、特殊标记:=或=、关键字range以及range表达式
for i, v := range "Go语言" {
fmt.Printf("%d: %c\n", i, v)
}
//携带range子句的for语句还可以应用于一个通道值之上;其作用是不断地从该通道值中接收数据,不过每次只会接收一个值。
//注意:如果通道值中没有数据,那么for语句的执行会处于阻塞状态;无论怎样,这样的循环会一直进行下去,直至该通道值被关闭,for语句的执行才会结束。
//break语句被执行时,会使其所属的for语句的执行立即结束
//continue语句被执行时,会使当次迭代被中止(当次迭代的后续语句会被忽略)而直接进入到下一次迭代
//goto 语句,将控制转移到被标记的语句
select语句:
select语句属于条件分支流程控制方法,不过它只能用于通道
- 它可以包含若干条case语句,并根据条件选择其中的一个执行
- select语句中的case关键字只能后跟用于通道的发送操作的表达式以及接收操作的表达式或语句
//如果该select语句被执行时通道ch1和ch2中都没有任何数据,那么肯定只有default case会被执行
//对于包含通道接收操作的case来讲,其执行条件就是通道中存在数据
//如果在当时有数据的通道多于一个,那么Go语言会通过一种伪随机的算法来决定哪一个case将被执行
//如果一条select语句中不存在default case, 并且在被执行时其中的所有case都不满足执行条件,那么它的执行将会被阻塞
// 当前流程的进行也会因此而停滞,直到其中一个case满足了执行条件,执行才会继续
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
select {
case e1 := <-ch1:
fmt.Printf("1th case is selected. e1=%v.\n", e1)
case e2 := <-ch2:
fmt.Printf("2th case is selected. e2=%v.\n", e2)
default:
fmt.Println("No data!")
}
//break语句也可以被包含在select语句中的case语句中
//它的作用是立即结束当前的select语句的执行,不论其所属的case语句中是否还有未被执行的语句
defer语句:
仅能被放置在函数或方法中,它由关键字defer和一个调用表达式组成
//defer它的确切的执行时机是在其所属的函数(这里是readFile)的执行即将结束的那个时刻
//无论readFile函数正常地返回了结果,还是由于在其执行期间有运行时恐慌发生而被剥夺了流程控制权,
//其中的file.Close()都会在该函数即将退出那一刻被执行
//当一个函数中存在多个defer语句时,它们携带的表达式语句的执行顺序一定是它们的出现顺序的倒序
func readFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return ioutil.ReadAll(file)
}
异常处理(error):
是Go语言内置的一个接口类型
//只要一个类型的方法集合包含了名为Error、无参数声明且仅声明了一个string类型的结果的方法,就相当于实现了error接口
type error interface {
Error() string
}
//创造出错误(即error类型的值)并把它传递给上层程序,只需调用标准库代码包errors的New函数即可
if path == "" {
return nil, errors.New("The parameter is invalid!")
}
异常处理(panic):
运行时恐慌
- 它只有在程序运行的时候才会被“抛出来”;并且,恐慌是会被扩散的;如果我们不显式地处理它的话,程序崩溃
- 内建函数panic可以产生一个运行时恐慌
- 内建函数recover就可以做到panic被恢复,recover函数必须要在defer语句中调用才有效
- 因为一旦有运行时恐慌发生,当前函数以及在调用栈上的所有代码都是失去对流程的控制权
- 只有defer语句携带的函数中的代码才可能在运行时恐慌迅速向调用栈上层蔓延时“拦截到”它。
//调用recover函数,该函数会返回一个interface{}类型的值,如果p不为nil,那么就说明当前确有运行时恐慌发生
//运行时恐慌代表程序运行过程中的致命错误,我们只应该在必要的时候引发它。
defer func() {
if p := recover(); p != nil {
fmt.Printf("Fatal error: %s\n", p)
}
}()
参考
https://www.runoob.com/go/go-tutorial.html
https://github.com/chentian114/goDemo1