GO语言学习(基础语法讲解)
博主推荐学习网站链接
这里为大家附上相关的文档链接,大家可以查询具体类型和语法的标准:文档链接
首先在实际开发中,我们需要明确这些概念,首先Go使用package
(和Python的模块类似)来组织代码(go语言在一定情况下是具有一定的python+c语言特征)。main.main()
函数(这个函数位于主包)是每个独立的可运行程序的入口点。Go使用UTF-8字符串和标识符(因为UTF-8的发明者是Go的发明者之一),所以它天生支持多语言。
现在开始讲解Go的基础语法:
变量
var value type
var value1,value2,value3 type
//上面的为定义不声明
var value type=tru_value //tru_value 是真实值,若不赋值则数为0,字符为’\0‘,布尔值则为false
var value1,value2,value3 type=v1,v2,v3 //实际上可以理解为3个同类型的值被同时赋值,值一一对应
在实际开发中为了减少代码的繁琐,Go语言开发者也进行了优化(可以根据赋的值自动确定变量类型),如下:
var value1,value2,value3=v1,v2,v3
value1,value2,value3:=v1,v2,v3
补充一下::=
这个符号直接替换了var
并且type
,形式通过简短的声明。不过它有一个限制,那就是它只能在函数内部使用;在函数外部使用桌面无法编译,所以一般用var
方式来定义全局变量。
下面我们来介绍一下一个特殊标志符(下划线_):_
(下划线)是一个特殊的标志名,任何赋予它的值都会被丢弃。老规矩上代码帮助你们理解:
_, b := 88, 35
在上面这个例子中,我们将35
赋予它值b
,并同时丢弃88
对于变量已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了i
但未使用。
package main
func main() {
var i int
}
下面罗列除出了一些默认值:
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 ""
此外go可以使用平行赋值,如下:i, j = i+1, j-1
常量
定义
:所谓常量,在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。
const constantName = value
const Pi = 3.1415926
const i = 10000
const MaxThread = 10
const prefix = "astaxie_"
//如果需要,也可以明确指定常量的类型(这个可以依据开发者具体需求来确定):
const Pi float32 = 3.1415926
自带类型讲解
注明一下:
Go 常量和一般程序语言不同的是,可以指定相当多的小数位数(例如200位), 若指定給float32自动缩短为32bit,指定给float64自动缩短为64bit。
整数
整数类型有无符号和带符号两种(一个区别在于是否包含负数)。Go同时支持int
和uint
,这两种类型的长度相同,但具体长度取决于不同编译器的实现。Go里面也有直接定义好位数的类型:rune
, int8
, int16
, int32
, int64
和byte
, uint8
, uint16
, uint32
, uint64
。其中rune
是int32
的别称,byte
是uint8
的别称。
浮点数
浮点数的类型有float32
和float64
两种(没有float
类型),默认是float64
。
复数
同python语言一样,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
。
//示例代码
var frenchHello string // 声明变量为字符串的一般方法
var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串
func test() {
no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量
japaneseHello := "Konichiwa" // 同上
frenchHello = "Bonjour" // 常规赋值
}
注意
:在Go语言中字符串是常量(不能直接改变),应该通过转化来改变字符串的值,常规修改代码如下:
s := "hello"
c := []byte(s) // 将字符串 s 转换为 []byte 类型
c[0] = 'c'
s2 := string(c) // 再转换回 string 类型
fmt.Printf("%s\n", s2)
Go语言可以允许使用+
来进行连接两个字符串(补充一点两边为整数则为加,两边为字符串则为连接)
s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)
高效修改代码如下:
s := "hello"
s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作
fmt.Printf("%s\n", s)
多行拼接字符串(这里需要使用到反引号)
m := `hello
world`
错误类型
特殊的是在Go内置有一个error
类型,专门用来处理错误信息,Go的package
里面还专门有一个包errors
来处理错误:
import "errors" //引入错误这个包
err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
fmt.Print(err)
}
优化代码
分组声明
one tip
:在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明:
例如原来代码为:
import "fmt"
import "os"
const i = 100
const pi = 3.1415
const prefix = "Go_"
var i int
var pi float32
var prefix string
进行分组声明,可以简化如下:
import(
"fmt"
"os"
)
const(
i = 100
pi = 3.1415
prefix = "Go_"
)
var(
i int
pi float32
prefix string
)
iota枚举
two tip
:Go里面有一个关键字iota
,这个关键字用来声明enum
的时候采用,它默认开始值是0,const中每增加一行加1,代码如下:
package main
import (
"fmt"
)
const (
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota"
)
const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0
const (
h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
)
const (
a = iota //a=0
b = "B"
c = iota //c=2
d, e, f = iota, iota, iota //d=3,e=3,f=3
g = iota //g = 4
)
func main() {
fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v)
}
注意
:除非被显式设置为其它值或iota
,每个const
分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是iota
,则它也被设置为iota
Go变量优化准则(***)
Go之所以会那么简洁,是因为它有一些默认的行为:
- 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。
- 大写字母开头的函数也是一样,相当于
class
中的带public
关键词的公有函数;小写字母开头的就是有private
关键词的私有函数。
数组 切片 映射
数组(array)
数组的定义中是可默认长度的,作为形参传入的其实是拷贝副本,因此不能使用指针,且长度也是声明的类型关键(意味着长度不一样类型也不一样),同理声明过程中的无值则为假(见上)代码如下:
var arr [n]type
var arr [10]int // 声明了一个int类型的数组
arr[0] = 42 // 数组下标是从0开始的
arr[1] = 13 // 赋值操作
fmt.Printf("The first element is %d\n", arr[0]) // 获取数据,返回42
fmt.Printf("The last element is %d\n", arr[9]) //返回未赋值的最后一个元素,默认返回0
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会自动根据元素个数来计算长度
// 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素,这个在最初赋值时应该具备其类型,以便编译器自动识别并成功为其赋值。
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
// 上面的声明可以简化,直接忽略内部的类型,同理也可以1进行自动处理
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
片(slice)
在集群数据中,由于数据写的相对封闭,因此为了灵活使用“动态集群”,我们在这里引用了切片,注意slice
并不是真正意义上的动态总线,而是一个引用类型。slice
总是指向一个简单的array
,slice
的声明也可以像array
一样,只是不需要长度(补充一下前面的[…]的用法只是交给编译器自行分配大小内存空间,而不是不分配大小)。代码如下:
// 和声明array一样,只是少了长度
var fslice []int
slice := []byte {'a', 'b', 'c', 'd'}
// 声明一个含有10个元素元素类型为byte的数组
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// slice可以从一个数据库或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数据库的开始位置,j是结束位置,但不包含array[j],它的长度是j-i,详细代码见下。
// 声明两个含有byte的slice
var a, b []byte
// a指向数组的第3个元素开始,并到第五个元素结束,
a = ar[2:5]
//现在a含有的元素: ar[2]、ar[3]和ar[4]
// b是数组ar的另一个slice
b = ar[3:5]
// b的元素是:ar[3]和ar[4]
注意
:slice和报表在报表时的区别:报表报表时,方括号内写明了堆栈的容量或使用...
自动计算长度,而报表slice
时,方括号内没有任何字符。(报表是go语言转化为其余文件格式的,比如Excel,Word等,应用在输出中),此外可以理解为切片是数组的另一种形式(将数组切片而成),引用类型是改变所有引用的值的,因此可以理解为切片可以理解在一起的,,可以互相影响。
一些补充说明:
slice的默认开始位置是0,ar[:n]等价于ar[0:n]
slice的第二个默认序列是存储的长度,ar[n:]等价于ar[n:len(ar)]
如果从一个数据库内部直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数据库的长度,即等价于ar[0:len(ar)]
对于slice有几个有用的内置函数:
len :获得slice的长度
cap ;获取slice最大容量
append向slice里面追加一个或者多个元素,然后返回一个和slice相同类型的slice
copy 函数copy从源slice的src(路径)中复制元素到目标dst,并返回复制的元素的个数
注意
:append
函数会改变slice
所引用的磁盘的内容,从而影响到引用相同磁盘的其他返回slice
。当slice
没有剩余空间(即(cap-len) == 0
)时,此时将动态分配新的磁盘空间。slice
磁盘指针将指向这个空间,而原磁盘的内容将保持不变;引用此其他磁盘的则不slice
重复。
从上面的概念来说slice
就像一个结构体,这个结构体包含了三个元素:
- 一个指针,指向内存中
slice
指定的开始位置 - 长度,即
slice
的长度 - 最大长度,显然
slice
开始位置到存储的最后位置的长度
Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
Slice_a := Array_a[2:5]
映射
map简洁Python中字典的概念,它的格式为
map[keyType]valueType
我们看下面的代码,map
的读取和设置也类似slice
一样,通过key
来,只不过slice
是index
`int`类型,而且map
很多类型,可以是int
,可以是string
及所有完全定义了==
与!=
操作的类型。
// 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化
var numbers map[string]int
// 另一种map的声明方式
numbers = make(map[string]int)
numbers["one"] = 1 //赋值
numbers["ten"] = 10 //赋值
numbers["three"] = 3
fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据
// 打印出来如:第三个数字是: 3
通过delete删除map的要素:
// 初始化一个字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true
csharpRating, ok := rating["C#"]
if ok {
fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
fmt.Println("We have no rating associated with C# in the map")
}
delete(rating, "C") // 删除key为C的元素
上面说了,map
也是一种引用类型,如果两个map
同时指向同一个键,那么改变一个,另一个也相应的改变:
m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut" // 现在m["hello"]的值已经是Salut了
可以得到以下结论:map类似为python中的字典,左键右值,map是无序的因此不能通过序列来获取,而要通过键来获取,是一种引用类型,故长度不一样,内置函数和slice类似,注意的是由于map线程不安全,因此在多个go-route中,应当使用互斥锁机制。
make,new操作
make
用于内建类型(map
、slice
和channel
)的内存分配。new
用于各种类型的内存分配。
new
:内建函数new
本质上说跟其他语言中的同名函数功能一样(可以参考c++的new):new(T)
分配了零值填充的T
类型的内存空间,并返回其地址,即一个*T
类型的值。用Go的术语来说,返回了一个指针,指向新分配的类型的T
零值,且new
返回指针。
make
:内建函数make(T, args)
与new(T)
具有不同的功能,make只能创建slice
、map
和channel
,并且返回一个有初始值(非零)的T
类型,而不是*T
。本质上来说,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个是包含一个slice
指向数据(内部array
)的指针、长度和容量的三项架构;在这些项目被初始化之前,slice
为、来说,初始化了内部的数据结构,填充适当的值,因此make
是返回初始化后的(非零)值。
流程和函数
if使用
基础语法可以参见c语言,这里罗列的是go的特殊用法(如下允许在条件判断语句里面声明一个变量,它的生命周期在这个条件判断语句里,外面就失效了,和c一点不同的就是判断条件不需要被括号包起来):
// 计算获取值x,然后根据x返回的大小,判断是否大于10。
if x := computedValue(); x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
//这个地方如果这样调用就编译出错了,因为x是条件里面的变量
fmt.Println(x)
// 多条件判断语法例子:
if integer == 3 {
fmt.Println("The integer is equal to 3")
} else if integer < 3 {
fmt.Println("The integer is less than 3")
} else {
fmt.Println("The integer is greater than 3")
}
goto使用
go中的goto是将语句跳转到当前函数内定义的标签位置处,非必要尽量不要使用此语法(标签名是大小写敏感的)。代码如下:
func myFunc() {
i := 0
Here: //这行的第一个词,以冒号结束作为标签
println(i)
i++
goto Here //跳转到Here去
}
for使用
在for循环中很多都是与c想通的,下面给出模版并进行讲解:
for valeu1;expression;value2 {
//...
}
value1是赋予的起始表达式,在1循环开始进行赋初值,value2一般是结束后执行操作,在循环结束后执行,expression是结束条件,若不满足则跳出循环。
for
喂养range
可以用于读取slice
和map
的数据:
for k,v:=range map {
fmt.Println("map's key:",k)
fmt.Println("map's val:",v)
}
为了简洁操作,可以省略如下:
sum := 1
for sum < 1000 {
sum += sum
}
这样子是不是很类似c中的while语法,这点也可以见得go语言的简洁性。
continue break操作
break
操作是跳出当前循环,continue
是跳过本次循环,在go语言中还可以,break
可以配合标签使用,即跳转到标签所指定的位置,代码如下:
for index := 10; index>0; index-- {
if index == 5{
break // 或者continue
}
fmt.Println(index)
}
// break打印出来10、9、8、7、6
// continue打印出来10、9、8、7、6、4、3、2、1
枚举
有些你需要写很多来if-else
实现的一些逻辑处理,这个时候代码一看就很丑的时候很冗长,而且也不容易以后的维护,因此go引入的·switch`就能很好的解决这个问题。代码如下:
i := 10
switch i {
case 1:
fmt.Println("i is equal to 1")
case 2, 3, 4:
fmt.Println("i is equal to 2, 3 or 4")
case 10:
fmt.Println("i is equal to 10")
default:
fmt.Println("All I know is that i is an integer")
}
我们把很多值聚合在一个case
里面,同时,里面switch
默认不是每个case
最后一个标记break
,匹配成功后不会自动继续执行其他case,而是跳出整个switch
,但是可以使用fallthrough
强制执行后面的case代码。代码如下:
integer := 6
switch integer {
case 4:
fmt.Println("The integer was <= 4")
fallthrough
case 5:
fmt.Println("The integer was <= 5")
fallthrough
case 6:
fmt.Println("The integer was <= 6")
fallthrough
case 7:
fmt.Println("The integer was <= 7")
fallthrough
case 8:
fmt.Println("The integer was <= 8")
fallthrough
default:
fmt.Println("default case")
}
结果输出为:
The integer was <= 6
The integer was <= 7
The integer was <= 8
default case
函数
函数模版如下:
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
//返回多个值
return value1, value2
}
解释上面的模版:
关键字func用于声明一个函数funcName
函数可以有一个或者多个参数,每个参数后面带类型,通过,分隔
函数可以返回多个值
返回上面值声明了两个变量output1和output2,如果你不想声明也可以,直接就两个类型
如果只有一个返回值且不声明返回值变量,那么你可以省略包括返回值的字符串
如果返回没有值,那么就直接省略最后的返回信息
如果有返回值,那么现在必须在函数的外层添加返回语句
建议:如果只是用了两个类型,我们也可以改成如下的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但是如果你的函数是导出的(首字母大写),最好命名返回值,不命名返回值,因为虽然使得代码更加简单了,不过可能会造成生成的文档的优势性差。代码如下:
func SumAndProduct(A, B int) (add int, Multiplied int) {
add = A+B
Multiplied = A*B
return
}
传入的类型也有区别:下面列出两种传入(值和指针)的区别:
传值为传入实参的一个副本,在改变参数的时候是不会对实参进行修改的,这点需要注意,数值只作用在副本上,示例:
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a int) int {
a = a+1 // 我们改变了a的值
return a //返回一个新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(x) //调用add1(x)
fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4"
fmt.Println("x = ", x) // 应该输出"x = 3"
}
【解释】
虽然我们调用了add1
函数,并且在add1
中执行a = a+1
操作,但是上面例子中x
变量的值没有发生变化,因为当我们调用add1
的时候,add1
接收的参数实际上是x
副本,而不是x
本身。这里牵扯到了变量的指针。我们知道,变量在内存中是存放在一定地址上的,修改变量实际是变量地址处修改变量的内存。只有add1
函数知道x
变量所在的地址,才能是x
变量的值。所以我们需要将x
所在地址&x
命名为函数,把函数参数的类型由int
改为*int
,即改为指针类型,才能在函数中修改x
变量的值。此时参数仍然是按复制指针的,只是复制一个指针。请看下面的例子:
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
*a = *a+1 // 修改了a的值
return *a // 返回新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(&x) // 调用 add1(&x) 传x的地址
fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
fmt.Println("x = ", x) // 应该输出 "x = 4"
}
优点:
传指针使得多个函数能够操作同一个对象。
传指针比较轻量级(8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话,在每次复制上面占用相对坐标的系统开销(内存和时间)。所以当你要传递大的就会结构体的时候,用指针是一个明智的选择。
Go语言中channel,,这种清晰类型的实现机制类似指针slice,map所以可以直接传递,而不用取地址后传递指针。(注:若函数需要改变slice的长度,则仍需要取地址传递指针)
扩展:变参
为了不确定参数的函数而引入的方法,代码如下:
func biancan(a...int){
func_block
}
上面的代码给出的是一些不确定个数的类型为整数的参数函数,使用中也有特殊的要求,如下:
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}
函数作为值、类型
在Go中函数也是一个变量,我们可以通过type
来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型,模版如下:
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
那就是可以把这个类型的函数当做值来传递,请看下面的例子
package main
import "fmt"
type testInt func(int) bool // 声明了一个函数类型
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main(){
slice := []int {1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // 函数当做值来传递了
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven) // 函数当做值来传递了
fmt.Println("Even elements of slice are: ", even)
}
优点当做值和类型在我们写一些通用接口的时候非常有用,通过上面的例子看到我们testInt
这个类型是一个函数类型,然后两个filter
函数的参数和返回值与testInt
类型是一样的,但是我们实现可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。
defer使用
延迟(defer)语句,当你在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回之前你需要关闭相应的资源,否则很容易造成资源丢失等问题。如下代码所示,我们一般写一个打开资源是这样操作的:
func ReadWrite() bool {
file.Open("file")
// 做一些工作
if failureX {
file.Close()
return false
}
if failureY {
file.Close()
return false
}
file.Close()
return true
}
而当我们使用了defer后就可以变得十分简洁,代码如下:
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX {
return false
}
if failureY {
return false
}
return true
}
如果有很多调用defer
,那么defer
就是采用后进先出的模式,代码如下:
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
所以下面的代码会输出4 3 2 1 0
异常与恢复机制
在实际开发中,Go不能像java一样发送异常,而是使用了panic的recover机制,这个机制相对来说比较考量代码的具体应用,因此请慎重的使用这个操作,下面给出一下异常和恢复的具体使用:
恐慌:
是一个内建函数,可以撤销原有的控制流程,进入一个
panic
状态中。当函数F
调用时panic
,函数F的执行被中断,但是F
中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F
的行为就像调用了panic
。这个过程继续向上,直到发生panic
的goroutine
中所有调用的函数返回也,此时程序退出。panic
可以直接调用panic
产生。由可以运行时错误产生,例如访问越界的数组。恢复:
是一个内建的函数,可以让进入
panic
状态的goroutine
恢复过来。recover
仅在延迟函数中有效。在正常的执行过程中,调用recover
会返回nil
,并且没有其他任何效果。如果当前的蜡goroutine
状态panic
,调用recover
可以捕获到panic
的输入值,并且恢复正常的执行。
下面这个函数演示了如何在过程中使用panic:
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
下面这个函数检查作为其参数的函数在执行时是否会产生panic:
func throwsPanic(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
return
}
结构体
利用结构体,我们可以创建自己的数据类型,结构类似1c语言的结构体。
type struct_name struct{
val1 type1
val2 type2
.....
}
使用需要先实例化,即var P struct_name
,这样我们就可以开始使用这个结构体的相关属性了,使用.
来使用结构体,代码如下:
type person struct {
name string
age int
}
var P person // P现在就是person类型的变量了
P.name = "Astaxie" // 赋值"Astaxie"给P的name属性.
P.age = 25 // 赋值"25"给变量P的age属性
fmt.Printf("The person's name is %s", P.name) // 访问P的name属性.
此外还可以使用,一下方式进行初始化:
P:=struct_name{value_tru,....}
P:=struct_name{val:value_tru}
P:=new(struct_name) // 来为结构体分配一个指针
匿名字段
我们上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段,当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那么默认Student就包含了Human的所有字段
speciality string
}
其实匿名了就相当与被匿名的已经被继承,可以实现这些字段的继承,实际上通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名字段的。
fmt.Println("His speciality is ", mark.speciality)
// 修改对应的备注信息
mark.speciality = "AI"
fmt.Println("Mark changed his speciality")
fmt.Println("His speciality is ", mark.speciality)
// 修改他的年龄信息
fmt.Println("Mark become old")
mark.age = 46
fmt.Println("His age is", mark.age)
// 修改他的体重信息
fmt.Println("Mark is not an athlet anymore")
mark.weight += 60
fmt.Println("His weight is", mark.weight)
mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1
实际上struct不仅仅能够将struct作为匿名字段,自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作,最外层的优先访问,也就是当你通过student.phone
访问的时候,是访问student里面的字段,而不是human里面的字段,这样就允许我们去重载通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。
type Skills []string
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,struct
Skills // 匿名字段,自定义的类型string slice
int // 内置类型作为匿名字段
speciality string
}
type Human struct {
name string
age int
phone string // Human类型拥有的字段
}
type Employee struct {
Human // 匿名字段Human
speciality string
phone string // 雇员的phone字段
}
func main() {
Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
fmt.Println("Bob's work phone is:", Bob.phone)
// 如果我们要访问Human的phone字段
fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}
本节结语
各位go语言的基础语法就先说到这里,后面会为大家带来面向对象编程,请各位友友继续支持,谢谢。
大家有不会的私我后台,有空必会,若是没回八成是没空,在这里也是希望大家多多在评论区中多讨论。