一.结构体
1.什么是结构体
结构体是用户定义的类型,表示若干个字段(Field)的集合。有时应该把数据整合在一起,而不是让这些数据没有联系。这种情况下可以使用结构体。
例如,一个职员有 firstName
、lastName
和 age
三个属性,而把这些属性组合在一个结构体 employee
中就很合理。
总的来说:结构体就是一系列属性的集合
2.结构体语法
语法
// type关键字 结构体名字 struct{}
一个基本的结构体,只包含属性
type Person struct { name string // 两个类型一样可以写一行 age,sex int }
3.结构体定义
定义时没有初始化查看他的空值
// 定义时没有初始化查看他的空值
var person Person
fmt.Println(person) // 输出:{ 0 0},他的空值是结构体属性的空值
结构体的空值是他每个字段的空值
由此可得,结构体是值引用类型,修改结构体的属性不会影响原值
定义并初始化的时候有两种传参方式
// 定义并初始化的时候有两种传参方式,他们的不同 var person Person = Person{name:"sxc"} // 关键字传参,可以传指定数量的 var person Person = Person{"sxc",18,1} // 位置传参,所有的参数都必须传,并且传递位置固定 fmt.Println(person.name)
关键字传参可以传任意数量的,位置传参必须都传
4.匿名结构体
匿名结构体:在main函数内部定义,只使用一次,可以用来存储经常使用的变量
// 分别定义两个变量并使用他们 name := "sxc" age := 18 fmt.Println(name,age)
当使用多次时,麻烦且不直观
//可以使用匿名结构体集成到一起,这样显示的更直观 student := struct { name string age int }{} student.name = "sxc" student.age = 18 fmt.Println(student.name,student.age)
5.结构体指针
Go语言帮我们做好处理,使用指针也可以点出对应的字段
// 结构体的指针 var person *Person = &Person{"sxc",18,1} fmt.Println(person) // 输出&{sxc 18 1},直观的显示 fmt.Println((*person).name) // 反解之后可以输出属性值 fmt.Println(person.name) // go帮我们做好处理,可以直接使用指针.属性的方式输出
6.匿名字段
匿名字段,类型名当做字段名,当我们创建结构体时,字段可以只有类型,而没有字段名。这样的字段称为匿名字段(Anonymous Field)。
// 匿名字段 type Test struct { string // 类型名当做字段名 int }
匿名字段的使用
// 匿名字段,类型名当做字段名 var test111 Test = Test{"hello",15} // 位置传参 var test123 Test = Test{string:"hello",int:15} // 关键字就是类型名 fmt.Println(test111.string) fmt.Println(test123.string)
7.结构体嵌套
结构体的字段有可能也是一个结构体。这样的结构体称为嵌套结构体。
type Person struct { name string // 两个类型一样可以写一行 age,sex int //hobby Hobby // 结构体嵌套 } // 定义一个Hobby结构体,在Person结构体中使用 type Hobby struct { id int hobbyname string }
定义和使用
// 结构体嵌套 var person Person = Person{name:"sxc",hobby:Hobby{id:5,hobbyname:"sing"}} // 结构体嵌套的定义 fmt.Println(person.hobby.hobbyname) // 结构体嵌套的使用
8.结构体嵌套+匿名字段(变量的提升)
type Person struct { name string // 两个类型一样可以写一行 age,sex int Hobby // 结构体嵌套+匿名字段 } // 定义一个Hobby结构体,在Person结构体中使用 type Hobby struct { id int //name string // 和上层结构体有同名字段 hobbyname string }
提升变量
// 结构体嵌套+匿名字段(用作变量提升) var person Person = Person{name:"sxc",Hobby:Hobby{id:5,hobbyname:"sing"}} fmt.Println(person.Hobby.hobbyname) fmt.Println(person.hobbyname) // 使用匿名字段后,我们可以在person这层就能直接点出Hobby的字段 fmt.Println(person.Hobby.name) // 注意当两个结构体中有同名字段时,内部结构体的字段不会提升
类似于面向对象的继承,子类可以使用父类的属性
9.结构体相等性(Structs Equality)
结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的。
package main import ( "fmt" ) type name struct { firstName string lastName string } func main() { name1 := name{"Steve", "Jobs"} name2 := name{"Steve", "Jobs"} if name1 == name2 { fmt.Println("name1 and name2 are equal") } else { fmt.Println("name1 and name2 are not equal") } name3 := name{firstName:"Steve", lastName:"Jobs"} name4 := name{} name4.firstName = "Steve" if name3 == name4 { fmt.Println("name3 and name4 are equal") } else { fmt.Println("name3 and name4 are not equal") } }
在上面的代码中,结构体类型 name
包含两个 string
类型。由于字符串是可比较的,因此可以比较两个 name
类型的结构体变量。
上面代码中 name1
和 name2
相等,而 name3
和 name4
不相等。该程序会输出:
name1 and name2 are equal name3 and name4 are not equal
如果结构体包含不可比较的字段,则结构体变量也不可比较。
package main import ( "fmt" ) type image struct { data map[int]int } func main() { image1 := image{data: map[int]int{ 0: 155, }} image2 := image{data: map[int]int{ 0: 155, }} if image1 == image2 { fmt.Println("image1 and image2 are equal") } }
在上面代码中,结构体类型 image
包含一个 map
类型的字段。由于 map
类型是不可比较的,因此 image1
和 image2
也不可比较。如果运行该程序,编译器会报错:main.go:18: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)。
二.方法
1.什么是方法
方法其实就是一个函数,在 func
这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。
下面就是创建一个方法的语法。
func (t Type) methodName(parameter list) {
}
2.给结构体绑定方法
// 给Person结构体加一个打印名字的方法 func (p *Person)changeName(a string) { (*p).name = a fmt.Println(p) }
person结构体
type Person struct {
name string
age,sex int
Hobby
}
3.为什么我们已经有函数了还需要方法呢?
Go 不是纯粹的面向对象编程语言
,而且Go不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。- 相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。
有了方法之后,我们就能使用结构体生成的对象点出方法来
// 可以用结构体生成的对象点出来使用 var p = Person{name:"sxc",Hobby:Hobby{hobbyname:"sing"}} p.printName()
这样结构体中拥有自己的字段属性,并且我们给他增加了方法,这样这个结构体就类似于面向对象的类,可以通过生成的对象点出来属性和方法
4.值接收器和指针接收器,两者何时使用
我们定义一个改名的方法
// 给Person结构体加一个打印名字的方法 func (p *Person)changeName(a string) { (*p).name = a fmt.Println(p) }
使用值和指针接收器调用该方法
// 值和指针类型都可以调方法 (&p).changeName("zzj") // 我们可以使用指针来调用该方法 p.changeName("zzj") fmt.Println(p.name) // 值调用时在方法内部改变了,但因为结构体是一个值类型,故没有真正的修改,指针调用时修改的是指针对应的内存地址,故真正的修改了
故:
当操作不需影响到结构体时使用值接收器
当操作需要影响到结构体时使用指针接收器
5.方法或函数,使用值或指针
a.在方法中使用值接收器
b.在函数中使用值参数
c.在方法中使用指针接收器
d.在函数中使用指针参数
定义四个对应的方法或函数
// 方法中使用值接收器 func (p Person)printName1 () { fmt.Println(p.name) } // 方法中使用指针接收器 func (p *Person)printName2 () { fmt.Println(p.name) } // 函数中使用值参数 func printName3 (p Person) { fmt.Println(p.name) } // 函数中使用指针参数 func printName4 (p *Person) { fmt.Println(p.name) }
各自调用上述方法或函数
//调用值接收器方法 p.printName1() //调用指针接收器方法 p.printName2() //调用值参数函数 printName3(p) //调用指针参数函数 printName4(p)
得出结论:
从上面四个不同的方法或函数中可得,值或者指针接收器都能调值或者指针方法
而函数指定传什么参数就需要传什么参数
6.给非结构体定义方法
首先尝试给int类型定义方法
//给非结构体定义方法 func (a *int)printNum () { *a++ }
发现int等基本类型都不支持自定义方法
我们使用起别名的方法
type MyInt int // 给int起别名
在给MyInt定义方法
func (a *MyInt) addNum () MyInt{ // 由于MyInt是值类型,故我们需要修改他的指针才能真正的修改值 *a++ return *a }
调用查看结果
var a MyInt = MyInt(6) // 初始化生成对象 fmt.Println(a.addNum()) // 调用add方法 fmt.Println(a)
两次打印的结果一致,故修改成功
三.接口
1.什么是接口
在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。
在 Go 语言中,接口就是方法签名(Method Signature)的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。这与面向对象编程(OOP)的说法很类似。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法。
简单来说:接口就是一系列方法的集合
2.接口的声明与实现
接口的语法
// 接口的语法
type 接口名 interface {
方法一
方法二
}
声明一个接口
// 定义一个鸭子接口
type Duck interface {
run()
speak()
}
接口的实现:只要在结构体中实现了接口中所有的方法就是实现了接口
首先定义一个结构体
// 定义高级鸭子结构体
type GDuck struct {
name string
age int
wife bool
}
然后实现接口中的方法
// 实现接口 func (p PDuck)run(){ fmt.Println("我是普通鸭子,我的名字叫",p.name) } func (p PDuck)speak(){ fmt.Println("我是普通鸭子,我嘎嘎叫") }
3.类型断言
我们需要不同的结构体实现接口中声明的speak方法
// 两种鸭子都要实现该方法,所以需要给鸭子这个接口类型写函数
func speak(d Duck){
d.speak()
}
这样只要调用这个函数,不同的鸭子类型都可以实现speak这个方法
但是当我们想要使用每个鸭子的具体的私有的属性时就不能完成了
这时候我们可以使用类型断言,我们断言他是某个类型的值
func speak(d Duck){ a := d.(GDuck) // 断言他是高级鸭子,这样就能使用高级鸭子的属性了 fmt.Println(a.name) a.speak() }
但是这样断言之后又出现了问题,如果是其他类型使用该函数,断言错误,就不能使用这个函数了
我们可以使用类型选择(Type Switch)来帮助我们完成断言
类型选择用于将接口的具体类型与很多 case 语句所指定的类型进行比较。它与一般的 switch 语句类似。唯一的区别在于类型选择指定的是类型,而一般的 switch 指定的是值。
// 上面这种方法虽然能使用某个鸭子的属性,但是只能使用具体的那一个鸭子,我们可以使用switch func speak(d Duck){ switch a:=d.(type) { case PDuck: fmt.Println(a.name) a.speak() case GDuck: fmt.Println(a.wife) a.speak() } } // 类型断言,使用switch
4.空接口
没有包含方法的接口称为空接口。空接口表示为 interface{}
。由于空接口没有方法,因此所有类型都实现了空接口。
package main import "fmt" // 空接口:一个方法都没有的接口 // 任何类型都可以使用该接口 type Empty interface { } func main() { var a = 5 var b = "sxc" var c = map[int]string{3:"sxc"} d := 5.95 //test(a) //test(b) Mytype(a) Mytype(b) Mytype(c) Mytype(d) var e Empty = 6 // 所有类型都赋值给了空接口类型 fmt.Println(e) } func test(e Empty){ fmt.Println(e) }
匿名空接口,可以接收任意类型的数据
func Mytype(e interface{}){ switch a:=e.(type) { case int: fmt.Println(a,"我是int类型") case string: fmt.Println(a,"我是sting类型") case map[int]string: fmt.Println(a,"我是map[int]string类型") default: fmt.Println(a,"不知道是什么类型") } }
5.指针接受者与值接受者
在接口(一)上的所有示例中,我们都是使用值接受者(Value Receiver)来实现接口的。我们同样可以使用指针接受者(Pointer Receiver)来实现接口。只不过在用指针接受者实现接口时,还有一些细节需要注意。
package main import "fmt" type Describer interface { Describe() } type Person struct { name string age int } func (p Person) Describe() { // 使用值接受者实现 fmt.Printf("%s is %d years old ", p.name, p.age) } type Address struct { state string country string } func (a *Address) Describe() { // 使用指针接受者实现 fmt.Printf("State %s Country %s", a.state, a.country) } func main() { var d1 Describer p1 := Person{"Sam", 25} d1 = p1 d1.Describe() p2 := Person{"James", 32} d1 = &p2 d1.Describe() var d2 Describer a := Address{"Washington", "USA"} /* 如果下面一行取消注释会导致编译错误: cannot use a (type Address) as type Describer in assignment: Address does not implement Describer (Describe method has pointer receiver) */ //d2 = a d2 = &a // 这是合法的 // 因为在第 22 行,Address 类型的指针实现了 Describer 接口 d2.Describe() }
在上面程序中的第 13 行,结构体 Person
使用值接受者,实现了 Describer
接口。
我们在讨论方法的时候就已经提到过,使用值接受者声明的方法,既可以用值来调用,也能用指针调用。不管是一个值,还是一个可以解引用的指针,调用这样的方法都是合法的。
p1
的类型是 Person
,在第 29 行,p1
赋值给了 d1
。由于 Person
实现了接口变量 d1
,因此在第 30 行,会打印 Sam is 25 years old
。
接下来在第 32 行,d1
又赋值为 &p2
,在第 33 行同样打印输出了 James is 32 years old
。棒棒哒。:)
在 22 行,结构体 Address
使用指针接受者实现了 Describer
接口。
在上面程序里,如果去掉第 45 行的注释,我们会得到编译错误:main.go:42: cannot use a (type Address) as type Describer in assignment: Address does not implement Describer (Describe method has pointer receiver)
。这是因为在第 22 行,我们使用 Address
类型的指针接受者实现了接口 Describer
,而接下来我们试图用 a
来赋值 d2
。然而 a
属于值类型,它并没有实现 Describer
接口。你应该会很惊讶,因为我们曾经学习过,使用指针接受者的方法,无论指针还是值都可以调用它。那么为什么第 45 行的代码就不管用呢?
其原因是:对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。但接口中存储的具体值(Concrete Value)并不能取到地址,因此在第 45 行,对于编译器无法自动获取 a 的地址,于是程序报错。
第 47 行就可以成功运行,因为我们将 a
的地址 &a
赋值给了 d2
。
程序的其他部分不言而喻。该程序会打印:
Sam is 25 years old James is 32 years old State Washington Country USA
6.实现多个接口
类型可以实现多个接口。
package main import ( "fmt" ) type SalaryCalculator interface { DisplaySalary() } type LeaveCalculator interface { CalculateLeavesLeft() int } type Employee struct { firstName string lastName string basicPay int pf int totalLeaves int leavesTaken int } func (e Employee) DisplaySalary() { fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf)) } func (e Employee) CalculateLeavesLeft() int { return e.totalLeaves - e.leavesTaken } func main() { e := Employee { firstName: "Naveen", lastName: "Ramanathan", basicPay: 5000, pf: 200, totalLeaves: 30, leavesTaken: 5, } var s SalaryCalculator = e s.DisplaySalary() var l LeaveCalculator = e fmt.Println(" Leaves left =", l.CalculateLeavesLeft()) }
上述程序在第 7 行和第 11 行分别声明了两个接口:SalaryCalculator
和 LeaveCalculator
。
第 15 行定义了结构体 Employee
,它在第 24 行实现了 SalaryCalculator
接口的 DisplaySalary
方法,接着在第 28 行又实现了 LeaveCalculator
接口里的 CalculateLeavesLeft
方法。于是 Employee
就实现了 SalaryCalculator
和 LeaveCalculator
两个接口。
第 41 行,我们把 e
赋值给了 SalaryCalculator
类型的接口变量 ,而在 43 行,我们同样把 e
赋值给 LeaveCalculator
类型的接口变量 。由于 e
的类型 Employee
实现了 SalaryCalculator
和 LeaveCalculator
两个接口,因此这是合法的。
该程序会输出:
Naveen Ramanathan has salary $5200
Leaves left = 25
7.接口的嵌套
尽管 Go 语言没有提供继承机制,但可以通过嵌套其他的接口,创建一个新接口。
package main import ( "fmt" ) type SalaryCalculator interface { DisplaySalary() } type LeaveCalculator interface { CalculateLeavesLeft() int } type EmployeeOperations interface { SalaryCalculator LeaveCalculator } type Employee struct { firstName string lastName string basicPay int pf int totalLeaves int leavesTaken int } func (e Employee) DisplaySalary() { fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf)) } func (e Employee) CalculateLeavesLeft() int { return e.totalLeaves - e.leavesTaken } func main() { e := Employee { firstName: "Naveen", lastName: "Ramanathan", basicPay: 5000, pf: 200, totalLeaves: 30, leavesTaken: 5, } var empOp EmployeeOperations = e empOp.DisplaySalary() fmt.Println(" Leaves left =", empOp.CalculateLeavesLeft()) }
在上述程序的第 15 行,我们创建了一个新的接口 EmployeeOperations
,它嵌套了两个接口:SalaryCalculator
和 LeaveCalculator
。
如果一个类型定义了 SalaryCalculator
和 LeaveCalculator
接口里包含的方法,我们就称该类型实现了 EmployeeOperations
接口。
在第 29 行和第 33 行,由于 Employee
结构体定义了 DisplaySalary
和 CalculateLeavesLeft
方法,因此它实现了接口 EmployeeOperations
。
在 46 行,empOp
的类型是 EmployeeOperations
,e
的类型是 Employee
,我们把 empOp
赋值为 e
。接下来的两行,empOp
调用了 DisplaySalary()
和 CalculateLeavesLeft()
方法。
该程序输出:
Naveen Ramanathan has salary $5200
Leaves left = 25
8.接口的零值
接口的零值是 nil
。故接口是引用类型。对于值为 nil
的接口,其底层值(Underlying Value)和具体类型(Concrete Type)都为 nil
。
package main import "fmt" type Describer interface { Describe() } func main() { var d1 Describer if d1 == nil { fmt.Printf("d1 is nil and has type %T value %v ", d1, d1) } }
上面程序里的 d1
等于 nil
,程序会输出:
d1 is nil and has type <nil> value <nil>
对于值为 nil
的接口,由于没有底层值和具体类型,当我们试图调用它的方法时,程序会产生 panic
异常。
package main
type Describer interface {
Describe()
}
func main() {
var d1 Describer
d1.Describe()
}
在上述程序中,d1
等于 nil
,程序产生运行时错误 panic
: panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xc8527] 。
四.异常处理和错误处理
1.异常处理
异常处理需要知道的三个参数
// 异常处理 // defer:无论如何都会在最后执行 // panic:主动抛出异常 // recover:恢复执行
代码示例
package main import "fmt" func main() { t1() t2() t3() } func t1() { fmt.Println("t1") } func t2() { defer func() { if a:= recover();a!=nil{ // a是错误信息 fmt.Println(a) // 打印错误信息 } // finally最后都会执行的话 fmt.Println("无论如何都会在最后执行") }() fmt.Println("t2") panic("主动抛出异常") //var a = []int{3,3} //fmt.Println(a[4]) // 取不到值会报错 fmt.Println("异常后面的信息") } func t3() { fmt.Println("t3") }
故最后异常处理我们只需要写
异常处理范本
defer func() { if a:= recover();a!=nil{ // a是错误信息 fmt.Println(a) // 打印错误信息 } // finally最后都会执行的话 fmt.Println("无论如何都会在最后执行") }()
2.错误处理
错误表示程序中出现了异常情况。比如当我们试图打开一个文件时,文件系统里却并没有这个文件。这就是异常情况,它用一个错误来表示。
代码示例
package main import ( "errors" "fmt" ) func main() { a,err := circle(-10) if err!=nil{ fmt.Println(err) } fmt.Println(a) } func circle(a int) (int, error){ if a < 0 { return 0,errors.New("出错了") // 出错返回正确类型的空值 } return 100,nil }
1.我们只需要在错误的情况中处理错误,并返回需要返回类型的零值和错误信息
2.在主代码中
判断传回来的err值是否为nil,如果不为nil就说明有错误,处理错误
如果为nil就表明没有错误,继续执行代码
105