Go简介
Go语言的创始人有三位,分别是图灵奖获得者、C语法共同发明人、Unix之父肯·汤普森(Ken Thompson)和Plan 9操作系统领导者、UTF-8原始设计者罗伯(Robber)编码。 Rob Pike 和 Robert Griesemer(Java 的 HotSpot 虚拟机和 Chrome 浏览器的 JavaScript V8 引擎的设计者之一)是主导设计的三位大人物。
Go语言是静态编译语言,但具有动态语言的高效率。它继承了C语言的表达式语法、控制结构、指针等。 Go语言引入了报表的概念。文件属于一个包,不能独立存在; Go语言自下而上支持高并发。
Go的应用领域:区块链应用、后端服务器应用、云计算和云服务开发。
基础语法
编程规范
Go语言官方推荐使用行注释来注释整个方法和语句;不同行的代码之间要有正确的缩进;gofmt
能够打印源代码;每行字符不能太多合理使用换行符。
Go的函数、变量、常量、自定义类型、包(package)的命名方式统称为标识符,标识符由数字,字母,下划线组成·,数字不能开头,严格区分大小写,_
是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循环有四个元素:循环的初始化、循环的条件和循环体。
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)
去参考数组切片:
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)
地图
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)
函数。
该映射是无序的,只能通过 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)
}
接口
- 接口介绍及使用
接口定义了对象的行为规范。它仅定义规范但不实现它。特定对象实现规范的细节。接口是方法的集合。接口的作用就像定义协议(规则)。它不关心属性(数据),只关心行为(方法)。
该接口具有以下特点:
- 接口是一个或多个方法签名的集合;
- 接口只有方法声明,没有实现,也没有数据字段;
- 接口可以匿名嵌入到其他接口中,或者嵌入到结构中;
- 只要一个对象实现了接口中的所有方法,那么它就实现了接口,而无需显式声明。
接口实现OOP功能的一般步骤如下:
案例:在下面的代码中,定义了设备中启动和结束的一组方法(接口)。接口不能直接通过方法实现,需要传递一个结构体。因此,一台计算机(Computer)和指向计算机结构(开始和结束)、键盘和鼠标的方法是相同的。
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
函数捕获错误,抛出错误信息并停止程序。