Go简介
Go语言的创始人有三位,分别是图灵奖获得者、C语法联合发明人、Unix之父肯·汤普森(Ken Thompson)、Plan 9操作系统领导者、UTF-8编码的最初设计者罗伯·派克(Rob Pike),以及Java的HotSpot虚拟机和Chrome浏览器的JavaScript V8引擎的设计者之一罗伯特·格瑞史莫(Robert Griesemer),三位大牛领导设计。
Go语言是一种静态编译语言,却有动态语言高效率,继承了c语言的表达式语法,控制结构,指针等;Go语言引入报的概念,一个文件归属一个包,不能独立存在;Go语言从底层支持高并发。
Go的应用领域:区块链应用、后端服务器应用、云计算和云服务开发。
基础语法
编程规范
Go语言官方推荐使用行注释来注释整个方法和语句;不同行的代码之间要有正确的缩进;gofmt
能够打印源代码;每行字符不能太多合理使用换行符。
Go的函数、变量、常量、自定义类型、包(package)的命名方式统称为标识符,标识符由数字,字母,下划线组成·,数字不能开头,严格区分大小写,_
是go的一个特殊字符,可以代表任何字符,但是其值会被忽略。不能用关键字当标识符。
关键字与保留字
- Go的关键字
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
- 保留字
true false iota nil
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
make len cap new append copy close delete
complex real imag
panic recover
定义变量
/*变量*/
var i = 10 //初始化定义
var j int
j = 10
fmt.Println("初始化声明定义:")
fmt.Println(i,j)
fmt.Println("未初始化定义:")
var a int
var b int64
fmt.Println(a,b)
fmt.Println("类型推导")
var c = "hello go"
fmt.Println(c)
fmt.Println("简洁写法")
d := 100
fmt.Println(d)
fmt.Println("多变量声明")
var x,y,z int = 10,11,12
fmt.Println(x,y,z)
fmt.Println("有初始化的变量声明")
var n1,n2,n3 = 100, "jack",12.5
fmt.Println(n1,n2,n3)
定义函数
func
关键字定义函数,如下所示
//func 关键字
func [methodName] ([参数列表及类型]) 返回值类型{
//代码块
return [返回值]
}
//例如主函数
package test
import "fmt"
func main(){
fmt.Println("Hello World")
}
定义函数的左中括号不能出现在下一行。以及所有形似该定义的方式
匿名函数
go支持匿名函数,如果某个函数只使用一次,可以使用匿名函数
匿名函数也可以赋值给一个变量,则改变了也变成了一个函数,可以通过该变量调用匿名函数:
这点和js语法有点像。
标识符大小写控制访问权限
可变参数
在函数中可以传递可变参数,Go语言支持可变参数,接收变参的函数时有着不定数量的参数,在定义函数的参数时,需要使其接收可变参数。可变参数如下定义:
func myfunc(args ...int){}
参数传递后会以切片的形式接收,对切片的操作可以实现对参数的运用:
func main(){
getSum(1,2,3,4,5)
}
func getSum(nums ... int) {
sum := 0
for i := 0; i < len(nums); i++ {
sum += nums[i]
}
fmt.Println("总和:",sum)
}
除了可变参数,go的指针传值也需要注意:
//主函数
func main(){
var a = 99
fmt.Println("定义的变量为:",a)
pin(&a)
fmt.Println("函数调用后变量为",a)
}
//方法·
func pin(p *int){
fmt.Println("传入时指变量的值",*p)
*p = 100
fmt.Println("传入后的指针变量的值",*p)
}
指针作为参数会改变变量的值。
返回值
go语言的函数支持多个返回值,而不需要使用数据结构如集合,数组等包装
package main
import "fmt"
func operation(n int, m int) (int, int, int, int) {
return n + m, n - m, n * m, n / m
}
func main() {
i, i2, i3, i4 := operation(4, 2)
fmt.Println(i, i2, i3, i4)
}
没有任何返回值就不写。
转义字符
(1)\\
一个\
,在go中\
为转义字符
(2) \t
一个制表位,实现对齐功能
(3) \n
换行符
(4) \r
一个回车位,从当前行最前行覆盖输出
import "fmt"
func main() {
//fmt.Println("Hello World")
fmt.Println("姓名\t年龄\t籍贯\t住址")
fmt.Println("john\t12\t河北\t北京")
}
注释
(1)行注释://注释文字
(2)块注释:/* 注释文字 */
,块注释不能嵌套
换行符
,
是go的手动换行符、\n
是go的自动换行符
import (
"fmt"
)
func main() {
//手动换行符
fmt.Println("螃蟹在剥我的壳,笔记本在写我。",
"漫天的我落在枫叶上雪花上。",
"而你在想我。")
//自动换行符
fmt.Println("螃蟹在剥我的壳,笔记本在写我。\n漫天的我落在枫叶上雪花上。\n而你在想我。")
}
数据类型
- 基本数据类型
在go中,字符本质是一个整数,直接输出时是该字符对应的utf-8编码的的码值,因此字符可以直接用int等基本数据类型类型接收,要输出字符需要格式化输出%d,%c
等。
数值型的各个关键字标识不同范围的类型,一个int4各字节,一个字节是8位,能表示的范围0-512,以此计算,各个类型的范围在合适的区间类选择相应类型。
go中没有专门的字符类型,字符都是在go用会转为位utf-8所对应的码值,因此字符直接用整数类型接收,需要注意的是不要字符的编码值要在数值类型所表示的范围内。
字符串有两种标识方法,""
为普通字符会识别特殊符号,`` 反引号为特殊字符,不会识别特殊符号,原样保存字符。字符串的拼接用+
,多个字符串拼接式+
需要留在上一行,不能作为开头。
基本数据类型未初始化是会有默认值的,如下图:
数据类型的转换
go的不数据类型的变量之间赋值需要显示转换,不能制动转换。也就是在转换时需要声明转换的类型。
语法表达式:类型(转换值)
//类型转换
var i int32 = 100
var num float32 = float32(i)
fmt.Println(num)
go中可以用_
丢弃不需要的变量(go中未使用的变量会报错)
//_忽略变量
var b,_ = "a", "err"
fmt.Println(b)
指针
变量分配的内存存储的是变量的值,变量之间的数据传递叫值传递,值传递会产生新的变量和内存空间,即对原变量的直接克隆;而引用是对已有变量的别名,对引用变量的操作极即为对原变量的操作,没有产生新的内存空间;而指针是指向内存空间的标识,实现了对该段内存空间的绝对控制,用于存储或操作变量。
三种变量的传值方式为值传递,引用传递,指针传递,都是为了优化内存空间和减少无效变量创建的。
&
用于获取变量的地址,*
用于定义指针,指针变量的本身为变量地址。
var a int = 10
var b = &a
fmt.Println("b变量的地址:",b)
var pointer * int = &a
fmt.Println("指针变量为:",pointer)
通过*
指针变量来取内存的值
var pin = *pointer
fmt.Println("指针的值为:",pin)
值类型都有对应的指针类型,形如*数据类型
,int对应的就是int,int64对应int64,类型要匹配。
访问权限
首字母的大小写限制访问权限。
包管理于引入包
- 包简介
包在go语言中十分重要,每个文件都是都属于一个包,go以包的形式来管理文件和项目目录结构。
包通过import
关键字引入,内置包直接通过函数名引入,其他通过路径引入import 包名/包名
,也可以在引入时为包重命名语法: import [名称] 包名/包名
- 引入内置包
//使用包的方法时许哟先引入包
import "包名"
import ([包名])
//一个go文件归属于一个包package
package main
- 引入自定义包
导入自定义模块必须要从根目录开始,不然go就会在配置的GOROOT下寻找,当作内置内置模块包来查找。
package main
import "fmt"
import "unit-2/src/main/demo04"
func main(){
var a = demo04.Add(1,4)
fmt.Println(a)
}
package demo04
func Add(a int,b int) int {
return a+b
}
go文件的命名在导入包的过程中为起到作用,主要是包名,和方法名,变量名等作为导入的内容。首字母大写为共有,首字母小写为私有。
在统一包下不能有相同的函数名,如果要编译成一个可执行文件,就需要将这个包声明为main,且存在主函数main函数。
go的每一个源文件都有一个init
函数,init函数会在main函数之前完成,每个init执行的顺序与引入的顺序有关。
运算符
大多数语言的运算符都功能都是差不多的。
流程管理
- 顺序结构
代码本身的执行流程为顺序结构。
switch语句用于基于不同条件执行不同动作,每个分支都是唯一的,从上到下注意测试,直到匹配位置,因此switch也可以看作是顺序结构。
switch 表达式{
case 表达式1,表达式2,... :
语句块1
case 表达式3,表达式4,... :
语句块2
// 若干case语句
default : 语句块
}
表达式可以是任意变量,常量,有返回值的函数等。
加
fallthrough
穿透,默认只能穿透下一层,执行一个case后会继续网后执行一个case。
- 分支结构
if 表达式{
//代码块
}else{
//代码块
}
表达式为true是就会执行{}的代码,{}不可省略,if后的{不能出现在下一行,else紧跟上一个{不能出现在第二行。表达式也可以用()括起来。
if 表达式{
//代码块
}
else if (表达式){
}{
.....
}else{
//代码块
}
省略号位置可以扩充任意else if,同样满足
{
不出现下一行。
import "fmt"
func main() {
var age int
fmt.Println("输入年龄:")
fmt.Scanln(&age)
idfi(age)
}
//判断年龄的分支结构
func idfi(a int){
if (a <= 18) {
fmt.Println("未成年")
}else if (a>18){
fmt.Println("已成年")
}else {
fmt.Println("输入错误")
}
}
- 循环结构
go的循环的关键字是for
,形如:
for i := 0; i < count; i++ {
fmt.Println("hello go")
}
func forTest(){
var count =10
for i := 0; i < count; i++ {
fmt.Println("hello go")
}
}
对于for循环有4个要素,循环的初始化,循环的条件,循环体。
for关键字后面是循环的条件,该条件可以被分块至其他位置,如下:
var j = 0
for j < 10 {
fmt.Println("hello")
j++
}
还有一种无线循环的写法,如下:
var i int
for{
i++
if(i == 100){
fmt.Println("100")
break
}
}
该循环等价于
for ; ;
由于没有结束条件,所以编程了死循环,需要借助break结束循环。
高阶for循环 for-range:
for-range循环自带索引和值,常用于遍历属猪,集合,切片等,其用法如下:
index,item := range [需遍历的值]
var str = "asdfghjA"
for index,val := range str{
fmt.Println(index,val)
}
字符会按utf-8的编码转换为对应的整数,所以需要格式化输出
%d
,%c
。在循环中常用到的的break,continue
分别是结束循环和跳过本次循环。
func gotoTest() {
fmt.Println("start")
goto programming1
for i := 0; i < 10; i++ {
fmt.Println("aaaaaaaaaa")
}
programming1:
fmt.Println("finish")
}
goto关键字后接程序跳转的入口,自定义命名,在任意位置引用该命名程序入口加:
即可跳转到该处。
闭包
闭包简单来说就是函数内部定义函数,内部函数调用外部函数的变量会使外层函数变量的值持久化。
数组
数组定义形如:
var arr_name [arr_length] type_name
arr_name 表示定义数组名称
arr_length 表示数组长度
type_name 表示数组元素存储的数据类型
var arr_name = [arr_length] type_name{value1,value2 ....}
var arr_name = [...] type_name {value1,value2...}
...
表示不定长度的数组
go语言也支持make函数定义数组,make函数可以动态创建数组,指定数组长度,容量以及数组元素类型:
var arr_name = make ([]type_name,length,capacity)
arr_name表示数组名称
type_name数组元素存储的数据类型
length数组长度
capacity 数组容量
var numbers = make([]int ,5 ,10)
切片
切片是对数组的抽象,其本身没有任何数据,只是对现有的数据的引用。
切片定义:
var identify []type
切片不需要说明长度(这是与定义数组的关键区别),或者使用make
函数来创建切片:
这个和python的切片是不一样的,python的切片可以对任意有索引的的数据结构直接使用,go既可定义切片也可以应用切片。
## python
a = [1,2,3,4,5]
b = a[:3]
print(b)
go对数组切片的引用:
sli := arr[start:end]
sli := arr[: end]
sli := arr[start:]
var arr1 = [5]int{1,2,3,4,5}
arr2 := arr1[:3]
fmt.Println(arr2)
map
var map1 map[int]string
var map2 := make(map[int]string)
var map3 := map[int]string{1:"go",2:"java"}
可以通过map[key]
来获取,由于当key不存在时,go会获取value的默认值,所以优势并不能按程序应有的结果走,需要判断一下,使用ok-idiom
即value,ok :=map[key]
。当然也可以直接通过默认值来判断,但这是需要知道value的类型的情况下,通过该类型的默认值判空。
var map1 map[int]string
var map2 = make(map[int]string)
var map3 = map[int]string{1:"go",2:"java"}
fmt.Println(map1,map2,map3)
val,ok := map3[1]
if ok {
fmt.Println("map3的值不为空,其值为:",val)
}else{
fmt.Println("map3的值为空")
}
val1:= map3[2]
if val1 == "" {
fmt.Println("map3为空")
}else{
fmt.Println(val1)
}
map通过key来添加和修改,如果存在key就修改,不存在就添加;删除1通过内置的delete(map,key)
函数。
map是无序的只能通过for-range来遍历:
var map3 = map[int]string{1:"go",2:"java",3:"python"}
for k,v := range map3{
fmt.Println(k,v)
}
结构体
结构体
数组可以存储同一类型的数据,结构体可以为不同项定义不同的数据类型,结构体是由一系列据具有相同或不同类型的数据构成的集合。
- 结构体的定义
结构体需要借助type
和struct
两个关键字定义,仍然同首字母的大小写来决定外部是否可以访问:
type Person struct{
name string
age int
sex string
address string
}
go语言抛弃了面向对象的那些复杂的特性,通过结构体来实现数据结构。另外结构体声明后就相当于自定义的数据类型,需要声明再使用。
- 结构体访问
结构体成员直接通过.
来访问,如下
package main
import "fmt"
func main() {
makeP1()
}
type Person struct{
name string
age int
sex string
address string
}
func makeP1(){
var p1 Person
p1.name = "_小许_"
p1.age = 21
p1.sex = "男"
p1.address = "河北"
fmt.Println(p1)
}
除了声明式定义外还啊看可以初始化定义:
//方法一
func makeP2(){
var p2 = Person{}
p2.name = "_小许_"
p2.age = 21
p2.sex = "男"
p2.address = "河北"
fmt.Println(p2)
}
//方法二
func makeP3(){
var p3 = Person{name:"_小许_",age:21,sex:"男",address:"河北"}
fmt.Println(p3)
}
//方法三
func makeP4(){
p4 := Person{
"小许",
21,
"男",
"河北",
}
fmt.Println(p4)
}
指针变量修改:
func makeP5(){
p5 := Person{
"小许",
21,
"男",
"河北",
}
fmt.Println("改变前p5:",p5)
var p *Person = &p5
(*p).age = 18
fmt.Println("改变后p5:",p5)
fmt.Println("指针变量的值:",*p)
}
结构体也可以通过new
关键字创建,该关键字是创建创建一段新的内存空间,相当于一一个*T
类型,作用是创建某种类型的指针函数。如下:
func makeP8(){
p8 := new(Person)
(*p8).name = "_xiaoxu_"
(*p8).age = 18
fmt.Println(*p8)
}
需要注意的是传递的参数是内存地址,通过指针类型修改。
- 结构体指针
go中没有opp面向对象的该概念,因此自定义的结构体将作为主要的对象来进行参数的传递。不同于java等语言,go既有值传递又有引用传递,引用传递需要借助指针来进行:
值传递,和引用传递需要区分,前者创建了新的内存空间,后者是对已定义的变量的地址引用。
//指针传递结构体
func makeBook3(book *Books){
fmt.Println("图书的名称:",(*book).name)
fmt.Println("图书的价格:",(*book).price)
fmt.Println("图书的作者:",(*book).author)
}
//函数返回Books类型
func reBooks() (Books){
book := Books{
name :"三国演义",
price : 22.1,
author : "罗贯中",
}
return book
}
func main() {
book := reBooks()
makeBook3(&book)
}
案例是对一个结构体指针传递的应用,主函数中book返回了一个Books结构体变量,makeBook3方法参数类型是Books的指针类型,在go中用*
定义指针,其类型是一个指针类型,指针是go的一种数据类型。在实际中,指针又是又是表示一个变量的地址,因此在参数传递时,传入的参数是地址(指针类型变量所表示的值)。
除了函数可以匿名,结构体也可以匿名,但是根据匿名的特性,是无法被调用的无法被修改的,所以只能在定义的地方赋值和使用:
//结构体测试
b := struct{
name string
age int
}{
//直接赋值
"_小许_",
22,
}
fmt.Println(b)
定义一个结构体就相当于在go源文件中定义了一个新的数据类型,这个数据类型可以像基本数据类型那样调用,但是需要按照结构体规则进行。
//结构体嵌套
type Liabary struct{
name string
address string
room Room
}
type Room struct{
name string
pid string
}
func makeRoom(){
//第一种创建方式
lia := Liabary{}
lia.name = "_xiaoxu_"
lia.address = "教八"
lia.room.name = "五楼"
lia.room.pid = "511"
fmt.Println(lia)
//第二种创建方式
lia2 := Liabary{
name : "aaa",
address : "bbb",
room : Room{name : "ccc",pid : "ddd"},
}
fmt.Println(lia2)
}
面向对象
面向对象的三大特性:继承,封装和多态。Go没有面向对象的概念,但是可以通过结构体
模拟面向对象的特性。
- 继承
已动物的继承关系模拟继承的代码:
目录结构:
/*
animals包
*/
package animals
//定义一个动物
type Animals struct{
name string
color string
age int
shot string
}
/*
狗继承动物
*/
type Dog struct{
animals Animals //结构体的嵌套模拟继承关系
foot string
teech string
}
package dog
import "unit-2/src/main/demo11/animals"
/*
宠物狗继承狗
*/
type PetDog struct{
animals animals.Animals
homedog string
listen string
}
package main
import "unit-2/src/main/demo11/dog"
import "unit-2/src/main/demo11/animals"
import "fmt"
/*
主函数代码
*/
func main() {
petDog := dog.PetDog{
animals : animals.Animals{
name :"小狗",
color : "白色",
age : 1,
shot : "旺旺...",
},
dog1: dog.Dog {
foot : "四条腿",
teech : "肉食动物",
},
homedog : "宠物狗",
listen : "听指挥",
}
fmt.Println(petDog)
}
案例的主程序代码,创建了小外层的对象,PetDog同时继承了Dog和Animals,但上述代码是由问题的,如下:
unknown field 'animals' in struct literal of type dog.PetDog
这是由于go的访问权限的限制,由于变量首字母的大小写决定了访问权限,所以大概就有三个部分的可访问性:结构体本身、结构体变量、结构体变量的成员变量在同包小无限制随意访问。有如下限制:
- 结构体名的大小写影响结构体本身的可访问性,首字母小写则包外不可见;
- 结构体变量名的首字母大小写 将影响其在包外的可访问性;
- 同包内,结构体变量的成员变量可随时被访问,不受首字母大小写的影响
相信很多学过Java的会疑惑,这和Java的访问权限控制系统private,protected,default,public不太一样啊,注意不要联想,每个语言都有自己的规则,按照规则记忆即可,Go的权限访问系统相对简单。
由于访问权限的限制,需要对代码重构,修改权限,如下:
/*
animals包下
*/
package animals
//定义一个动物
type Animals struct{
Name string
Color string
Age int
Shot string
}
//狗继承动物
type Dog struct{
Danimals Animals
Foot string
Teech string
}
/*
dog包下
*/
package dog
import "unit-2/src/main/demo11/animals"
//定义宠物狗
type PetDog struct{
Pdog animals.Dog
Homedog string
Listen string
}
/*
主函数包下
*/
package main
import "unit-2/src/main/demo11/dog"
import "unit-2/src/main/demo11/animals"
import "fmt"
//主函数
func main() {
//最外层宠物狗
petDog := dog.PetDog{
//内层狗
Pdog: animals.Dog {
//动物类
Danimals : animals.Animals{
Name :"小狗",
Color : "白色",
Age : 1,
Shot : "旺旺...",
},
Foot : "四条腿",
Teech : "肉食动物",
},
Homedog : "宠物狗",
Listen : "听指挥",
}
fmt.Println(petDog)
}
代码重构只是将结构体和结构体变量的首字母改为大写或首字母大写的重命名,改完后在外包也可以访问到,报错消失:
运行成功:
这家伙比面向对象编程的继承可难搞多了,继承三层就这莫深了,难搞喔! 😒
- 封装
在上面的代码中已经基本显示了go语言结构体,变量,函数的封装性。由于go的访问权限的限制,由于变量首字母的大小写决定了访问权限,所以大概就有三个部分的可访问性:结构体本身、结构体变量、结构体变量的成员变量在同包小无限制随意访问。有如下限制:
- 结构体名的大小写影响结构体本身的可访问性,首字母小写则包外不可见;
- 结构体变量名的首字母大小写 将影响其在包外的可访问性;
- 同包内,结构体变量的成员变量可随时被访问,不受首字母大小写的影响;
- 方法和变量具有上述同样的特性。
相信很多学过Java的会疑惑,这和Java的访问权限控制系统private,protected,default,public不太一样啊,注意不要联想,每个语言都有自己的规则,按照规则记忆即可,Go的权限访问系统相对简单。
- 多态
在编写好多结构体后,实例化结构体会产生不同的对象,如下:
package main
import "unit-2/src/main/demo11/dog"
import "unit-2/src/main/demo11/animals"
import "fmt"
//主函数
func main() {
/*
多态性体现
*/
//最外层宠物狗
petDog := dog.PetDog{
//内层狗
Pdog: animals.Dog {
//动物类
Danimals : animals.Animals{
Name :"泰迪",
Color : "白色",
Age : 1,
Shot : "旺旺...",
},
Foot : "四条腿",
Teech : "杂食动物",
},
Homedog : "宠物狗",
Listen : "听指挥",
}
fmt.Println(petDog)
petDog1 := dog.PetDog{
//内层狗
Pdog: animals.Dog {
//动物类
Danimals : animals.Animals{
Name :"二哈",
Color : "黄色",
Age : 1,
Shot : "呜呜...",
},
Foot : "四条腿",
Teech : "肉食动物",
},
Homedog : "宠物狗",
Listen : "不听听指挥",
}
fmt.Println(petDog1)
}
多态性将在接口章节具体叙述。
方法与函数
- 函数
函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。函数可以返回任意数量的返回值。
func test(x, y int, s string) (int, string) {
// 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
n := x + y
return n, fmt.Sprintf(s, n)
}
函数参数
函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
- 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
- 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
- 方法
Golang 方法总是绑定对象实例,并隐式将实例作为第一实参。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
简单来说就是方法必须依赖于某个变量,可以是命名类型或者结构体类型的一个值或者是一个指针,而函数可以单独存在。
这是对象定义函数的体现,变量,结构体内部不能定义函数,需要在外部定义函数指向变量,如下
说到方法依赖于某个变量,那么其调用也必须通过变量该调用,不能直接像函数一样调用:
package main
import "fmt"
type Person struct{
name string
age int
}
//定义方法
func (per Person) futrue( age int){
age = per.age
if(age > 18){
fmt.Println("上大学")
}
if(age < 18){
fmt.Println("上小学")
}
}
func main(){
p1 := Person{
"_小许_",
20,
}
fmt.Println(p1)
p1.futrue(0)
}
在上面的程序中,定义了一个futrue方法指向Person结构体,通过指向的结构体变量参数可以调用结构体的成员变量,通过方法的参数可以于结构体参数交互。
go包含指针的概念,所以任何关于参数传递都有两种方式,值传递和指针传递,这里指针传递自行思考。
方法的意义在于,实现类的行为,模拟面向对象编程的多态。
go结构体模拟继承中,方法通过指向不同的接收者也实现了方法的继承,方法名同的话,子类方法会覆盖父类方法(方法的重写)。
package main
import "fmt"
type Person struct{
name string
age int
}
//定义方法
func (per Person) futrue( age int){
age = per.age
if(age > 18){
fmt.Println("上大学")
}
if(age < 18){
fmt.Println("上小学")
}
}
//定义Person的子类
type Student struct{
person Person
address string
}
//重写父类的方法
func (stu Student) futrue(age int){
age = stu.person.age
if(age>= 3 && age <7 ){
fmt.Println("孩子在上学前班")
}
if(age >= 7 && age <=13){
fmt.Println("孩子在上小学")
}
if(age >13 && age <= 16){
fmt.Println("孩子在上初中")
}
if(age >16 && age <= 19){
fmt.Println("孩子在上高中")
}
if(age > 19 && age <= 23){
fmt.Println("孩子在上大学")
}
if(age >23){
fmt.Println("待续...")
}
}
func main(){
p1 := Person{
"_小许_",
20,
}
fmt.Println(p1)
p1.futrue(0)
s1 := Student{
person:Person{
"_小许_",
21,
},
address : "河北",
}
fmt.Println(s1)
s1.futrue(0)
}
接口
- 接口介绍及使用
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。interface是一组method的集合,接口做的事情就像是定义一个协议(规则),不关心属性(数据),只关心行为(方法)。
接口具有以下特性:
- 接口是一个或多个方法签名的集合;
- 接口只有方法声明,没有实现,没有数据字段;
- 接口可以匿名嵌入其他接口,或嵌入到结构中;
- 一个对象只要全部实现了接口中的方法,那么就实现了这个接口,无需显示声明。
接口实现OOP的特性的一般步骤如下:
案例:在如下代码中,定义了装置中作和结束的方法集(inteface),通过方法不能直接实现接口,需要通过结构体,于是创建了电脑(Computer)以及指向电脑结构体的方法(start和end),Keyboard,Mouse也是同样的道理。
package main
import "fmt"
func main(){
//电脑对象实例化
computer := Computer{}
computer.start()
computer.end()
//鼠标对象实例化
mouse := Mouse{}
mouse.start()
mouse.end()
//键盘
keyboard := Keyboard{}
keyboard.start()
keyboard.end()
}
//定义设备接口
type Device interface{
start()
end()
}
//定义鼠标结构体实现类
type Mouse struct {
name string
}
//定义键盘结构体实现类
type Keyboard struct{
name string
}
//定义电脑结构体实现类
type Computer struct{
name string
}
/*
鼠标类实现接口方法
*/
func (m Mouse) start() {
m.name = "鼠标"
fmt.Println("鼠标准备就绪!")
}
func (m Mouse) end() {
m.name = "鼠标"
fmt.Println("鼠标已正常使用!")
}
func (k Keyboard) start() {
k.name = "键盘"
fmt.Println("键盘准备就绪!")
}
func (k Keyboard) end() {
k.name = "键盘"
fmt.Println("键盘已正常使用!")
}
func (c Computer) start() {
c.name = "电脑"
fmt.Println("电脑准备就绪!")
}
func (c Computer) end() {
c.name = "电脑"
fmt.Println("电脑已正常使用!")
}
接口是一个方法集,接口方法作用对象为结构体,结构体通过指向结构体的方法即与接口相同的方法产生关系。
上面是通过方法将接口与结构体联系起来,并通过结构体的对象来调用属性和方法,其实接口也是可以直接使用的,这也是Go模拟面向对象的多态性的特性。
//直接使用接口
var device1 Device
device1.start()
device1.end()
空指针异常,方体都是空的。
//接口实现其实现类的实例
var device2 Device
device2 = Mouse{"鼠标"}
device2.start()
device2.end()
这是面向对象的多态性的体现。
- 空接口
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
package main
import "fmt"
func main() {
// 定义一个空接口x
var x interface{}
fmt.Printf("type:%T\n", x)
var y interface{
start()
}
fmt.Printf("type:%T\n", y)
var z interface {
start() int
}
fmt.Printf("type:%T\n", z)
}
不论接口中有没有方法或者方法返回值是何种类型,接口类型都是nil(空)类型,只有在具体实现时才有实际意义,接口的类型取决于实现类的类型。
Go 语言 中 函数 可以不返回任何值,也可以返回一个或者多个值,没有返回任何的值,因此没有返回值列表,同时,在函数体里面不需要使用 return 语句。因此接口有一个或若干方法,就确定了接口的某个方法一定是那种类型,那么在接口作为参数时只能传递接口方法返回值的类型。
空接口没有任何方法,也就没有相应的方法返回值,所以其类型完全取决于赋值的类型。因此空接口类型的变量可以存储任意类型的变量。也可以说任何类型都实现了空接口。
package main
import "fmt"
func main() {
// 定义一个空接口x
var x interface{}
fmt.Printf("type:%T\n", x)
//y
var y interface{
start()
}
fmt.Printf("type:%T\n", y)
//y
var z interface {
start() int
}
fmt.Printf("type:%T\n", z)
s := "_小许_"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 21
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}
空接口可以赋值任意类型的变量,那么其作为参数就可以接收任意类型的参数。
空接口可以赋值任意类型,那么在其作为为参数时如何确定其类型呢?Go提供了接口断言
来得到其类型。
- 确定类型的接口作为参数
package main
import "fmt"
func main(){
var p1 Which
p1 = Phone{"小米","性价比高"}
fmt.Println("name:",p1.getName()+" "+"usage:",p1.getUsage)
showWhich(p1)
}
//接口解释
type Which interface{
getName() string
getUsage() string
}
type Phone struct{
name string
usage string
}
func (p Phone) getName() string {
return p.name
}
func (p Phone) getUsage() string {
return p.usage
}
//接口方法
func showWhich(w Which){
fmt.Printf("name:%s,usage:%s",w.getName(),w.getUsage())
}
在定义showWhich方法时参数为Which类型,由于Phone是Which的实现类,因此在传参是也能传递。类型是确定的。
- 空接口作为参数
package main
//import "fmt"
func main() {
}
//空接口
type Who interface{}
//定义方法参数为空接口
func getWho(w Who) {
//
}
在上面代码中getWho方法传递的参数是Who由于其没有实现类,不知道其类型,无法继续编写代码。想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T)
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。
所以当空接口的类型很多时就可能需要多层类型判断,如下:
package main
import "fmt"
func main() {
getWho("hi")
}
//空接口
type Who interface{}
//定义方法参数为空接口
func getWho(w Who) {
//类型断言
str,ok := w.(string)
if(ok){
fmt.Println(str+"Hello World")
}else{
num,ok := w.(int)
if(ok){
fmt.Println(num*100)
}else{
fmt.Println("参数既不是字符串也不是数字")
}
}
}
牢记
x.(T)
空接口断言语法。
自定义类型
Go语言中允许使用type
关键字自定义类型,或类型别名。
- 类型定义:
type 类型名 Type
- 类型别名:
type 别名=Type
type关键字在go中有很重要的作用,这里不再深入了解了。
错误处理机制
Go程序中出现问题统称为错误,没有其他语言分的那么详细。错误是非常严重的问题,错误会导致程序终止,但是错误也是可以通过程序处理的,并抛出错误信息。
- 错误捕获
Go语言中使用defer
和recover
来捕获错误,并通过匿名函数抛出错误信息。
如下图代码,除0是错误的,是显性的,但实际可以并不知到该代码会出错,那么如何对该段可能出错的代码进行错误捕获呢?
num1 := 10
num2 := 0
result := num1/num2
fmt.Println(result)
Go语言中使用defer
和recover
来捕获错误,并抛出错误,需要注意的是defer
和recover
需要定义在函数中,用来抛出特定的有意义的错误:
import "fmt"
func main(){
fmt.Println("Hello World")
//调用有异常的函数
test()
fmt.Println("程序执行了!")
}
func test() {
//匿名函数处理错误
defer func () {
err:= recover()
if err != nil{
fmt.Println("err",err)
}
}()
num1 := 10
num2 := 0
result := num1/num2
fmt.Println(result)
}
这样处理错误的好处是不会影响整个程序的执行,只是程序的某些功能发生异常,整个程序还在运行着。匿名函数中可以通过日志和控制台来打印错误。
- 自定义错误
error
Go语言内置的错误信息,一般不满足全部的开发需求,Go也提供了自定义错误,来扩展错误。
自定义错误更为灵活,但是自定义错误需要程序员去写逻辑判断,并返回错误信息。由于程序需要返回错误信息的话函数的返回值就必须是一个error
错误类型:
//自定义错误
func readFile(name string) error {
if name == "conf.yml" {
return nil
}else {
return errors.New("配置文件读取失败!")
}
}
//主函数调用
fmt.Println("Hello World")
err := readFile("aaa")
fmt.Println(err)
遇到爆裂性的错误,需要是程序停止的使用panic(error)
函数抛出错误并停止程序:
//主函数捕获错误改造
err := readFile("aaa")
//fmt.Println(err)
if(err != nil){
panic(err)
}
fmt.Println("Hello World")
抛出错误后程序停止,后面代码也没执行。
总结:
- Go语言使用
defer
和recover
来捕获错误,通过匿名函数抛出错误;- Go的错误类型为
error
,支持位于errors
包下的New()
方法自定义错误信息。- Go中通过
panic
函数捕获错误,抛出错误信息并停止程序。