文章目录
结构体struct
Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。结构体可以存储一组不同类型的数据,是一种复合类型
1.声明
type Programmer struct {
Name string
Age int
Job string
language []string
}
type Rec struct{
height,width,area int
color string
}
在声明结构体字段时,字段名和方法名不应该重复
结构体本身以及其内部的字段都遵守大小写命名的暴露方式
2.创建
Go不存在构造方法,大多数情况下采用如下的方式来创建。
package main
type Programmer struct {
Name string
Age int
Job string
language []string
}
func main() {
Jack := Programmer{
Name: "Jack",
Age: 18,
Job: "医生",
language: []string{"中文", "英语"},
}
//可省略 字段名称
xiaowang := Programmer{
"xiaowang",
19,
"护士",
[]string{"中文"},
}
}
当省略字段名称时,就必须初始化所有字段,且必须按照声明的顺序初始化。
func NewProgrammer() Programmer {
return Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}
}
也可以编写一个函数来专门初始化结构体,这类函数通常有另一个名称:工厂方法,
3.组合
在Go中,结构体之间的关系是通过组合来表示的,可以显式组合,也可以匿名组合,后者使用起来更类似于继承,但本质上没有任何变化。例如:
//显示的组合
package main
import "fmt"
type Programmer struct {
Name string
Age int
Job string
language []string
}
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}
func main() {
//在使用时需要显式的指定字段p
student := Student{
p: Person{name: "小王", age: 18},
school: "北大",
}
fmt.Println(student)
//
}
//匿名组合
匿名字段的名称默认为类型名,调用者可以直接访问该类型的字段和方法,但除了更加方便以外与第一种方式没有任何的区别。
package main
import "fmt"
type Programmer struct {
Name string
Age int
Job string
language []string
}
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}
func main() {
//在使用时需要显式的指定字段p
student := Student{
Person: Person{name: "小王", age: 18},
school: "北大",
}
fmt.Println(student)
//
}
指针
对于结构体指针而言,不需要解引用就可以直接访问结构体的内容,例子如下:
package main
import "fmt"
type Programmer struct {
Name string
Age int
Job string
language []string
}
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}
func main() {
p := &Person{
name: "xxx",
age: 12,
}
fmt.Println(p.name)
}
在编译的时候会转换为(*p).name
,(*p).age
,其实还是需要解引用,不过在编码的时候可以省去,算是一种语法糖。
标签
结构体标签是一种元编程的形式,结合反射可以做出很多奇妙的功能,格式如下
`key1:"val1" key2:"val2"`
标签是一种键值对的形式,使用空格进行分隔。结构体标签的容错性很低,如果没能按照正确的格式书写结构体,那么将会导致无法正常读取,但是在编译时却不会有任何的报错,下方是一个使用示例。
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}
结构体标签最广泛的应用就是在各种序列化格式中的别名定义,标签的使用需要结合反射才能完整发挥出其功能。
空结构体
空结构体没有字段,不占用内存空间,可以通过unsafe.SizeOf
函数来计算占用的字节大小
func main() {
type Empty struct {
}
fmt.Println(unsafe.Sizeof(Empty{}))//0
}
函数
函数可以直接通过func
关键字来声明,也可以声明为一个字面量,也可以作为一个类型。
1.声明
//直接申明
func doSomeThing(){
}
//字面量
var doSome func()
//类型
type DoAnything func()
函数签名由函数名称,参数列表,返回值组成,下面是一个完整的例子
func Sum(a,b int)int{
return a+b
}
在Go中,不支持函数重载
package main
import "fmt"
func main() {
//变长参数可以接收0个或多个值,必须声明在参数列表的末尾。
fmt.Println(doSome(1, 2, 3, 44))
}
func doSome(args ...int) int {
sum := 0
for i := 0; i < len(args); i++ {
sum += args[i]
}
return sum
}
Go中的函数参数是传值传递,即在传递参数时会拷贝实参的值
2.返回值
Go中的返回值也可以有名称,并且可以拥有多个返回值。
有多个返回值则需要加上括号
package main
import "fmt"
func main() {
fmt.Println(doMoreThing(1, 2, 3))
}
// 多个返回值
func doMoreThing(args ...int) (string, int) {
sum := 0
for i := 0; i < len(args); i++ {
sum += args[i]
}
return "这是结果", sum
}
如果返回值有名称,也需要加上括号。
func Sum(a, b int) (ans int) {
return a + b
}
//或者
func Sum(a, b int) (ans int) {
ans = a + b
return // 等价于 return ans
}
3.匿名函数
匿名函数只能在函数内部存在,匿名函数可以简单理解为没有名称的函数,例如
package main
import "fmt"
func main() {
func() {
fmt.Println("匿名函数")
}()
}
或者当函数参数是一个函数类型时,这时名称不再重要,可以直接传递一个匿名函数
package main
import "fmt"
func main() {
i := doSome(1, 2, func(a, b int) int {
return a + b
})
fmt.Println(i)
}
func doSome(a, b int, c func(int,int) int) int {
return c(a, b)
}
4.闭包
在一些语言中又被称为Lamda表达式,经常与匿名函数一起使用,函数 + 环境引用 = 闭包
package main
import "fmt"
func main() {
sum := Sum(1)
fmt.Println(sum(1, 1))
fmt.Println(sum(1, 1))
fmt.Println(sum(1, 1))
}
func Sum(a int) func(int, int) int {
sum := a
return func(i int, i2 int) int {
sum += i + i2
return sum
}
}
3
5
7
匿名函数引用了参数sum
,即便Sum
函数已经执行完毕,虽然已经超出了它的生命周期,但是对其返回的函数传入参数,依旧可以成功的修改其值,这一个过程就是闭包。事实上参数sum
已经逃逸到了堆上,只要其返回值函数的生命周期没有结束,就不会被回收掉。
费波那契数列
package main
import "fmt"
func main() {
fib := Fib()
for i := 0; i < 10; i++ {
fmt.Println(fib())
}
}
func Fib() func() int {
a, b := 1, 1
return func() int {
a, b = b, a+b
return a
}
}
延迟调用
defer
package main
import "fmt"
func main() {
defer fmt.Println("defer")
fmt.Println(123)
}
打印结果
123
defer
当有多个defer
语句时,会按照后进先出的顺序执行。
package main
import "fmt"
func main() {
defer fmt.Println("defer1")
defer fmt.Println("defer2")
defer fmt.Println("defer3")
defer fmt.Println("defer4")
fmt.Println(123)
}
123
defer4
defer3
defer2
defer1
延迟调用通常用于释放文件资源,关闭连接等操作,另一个常用的写法是用于捕获panic
方法
方法与函数的区别在于,方法拥有接收者,而函数没有,且只有自定义类型能够拥有方法
import "fmt"
func main() {
var i IntSlice = []int{1, 2, 3}
fmt.Println(i.getItem(0))
fmt.Println(i.setItem(0, 100))
fmt.Println(i.len())
}
type IntSlice []int
func (i IntSlice) getItem(index int) int {
return i[index]
}
func (i IntSlice) setItem(index, val int) IntSlice {
i[index] = val
return i
}
func (i IntSlice) len() int {
return len(i)
}
先声明了一个类型IntSlice
,其底层类型为[]int
i
就是接收者,IntSlice
就是接收者的类型,接收者就类似于其他语言中的this
或self
,只不过在Go中需要显示的指明。
方法的使用就类似于调用一个类的成员方法,先声明,再初始化,再调用。
值接收者
package main
import "fmt"
type MyInt int
func (i MyInt) Set(val int) {
i = MyInt(val) // 修改了,但是不会造成任何影响
}
func main() {
myInt := MyInt(1)
myInt.Set(2)
fmt.Println(myInt)//1
}
通过指针调用会如何
package main
import "fmt"
type MyInt int
func (i MyInt) Set(val int) {
i = MyInt(val) // 修改了,但是不会造成任何影响
}
func main() {
myInt := MyInt(1)
(&myInt).Set(2)
fmt.Println(myInt)//1
}
遗憾的是,这样的代码依旧不能修改内部的值,为了能够匹配上接收者的类型,Go会将其解引用,解释为(*(&myInt)).Set(2)
。
指针接收者
稍微修改了一下,就能正常修改myInt
的值。
package main
import "fmt"
type MyInt int
func (i *MyInt) Set(val int) {
*i = MyInt(val) //
}
func main() {
myInt := MyInt(1)
myInt.Set(2)
fmt.Println(myInt)
}
接口
什么是方法集,方法集就是一组方法的集合,同样的,类型集就是一组类型的集合。
1.基本接口
只包含方法集的接口就是基本接口
package main
import "fmt"
// 定义一个接口 包含两个方法work和play
type doSome interface {
addAge()
downName()
}
type person struct {
name string
age int
}
func (p *person) addAge() {
p.age += 1
}
func (p *person) downAge() {
p.age -= 1
}
//person实现了doSome接口
type Construction struct {
person *person
}
func (c *Construction) Build() {
c.person.addAge()
//c.person.downAge()
}
func main() {
xiaozhang := person{
name: "小张",
age: 12,
}
xiaowang := person{
name: "小王",
age: 14,
}
person1 := &Construction{&xiaozhang}
person2 := &Construction{&xiaowang}
person1.Build()
person2.Build()
fmt.Println(xiaozhang.age)
fmt.Println(xiaowang.age)
}
2.通用接口
只要包含类型集的接口就是通用接口
泛型
package main
func main() {
Sum[int](1, 23)
}
func Sum[T int | float64](a, b T) T {
return a + b
}
T代表一个类型形参,形参具体是什么类型取决于传进来什么类型
**类型形参:**T
**类型约束:**int|float64
类型实参:Sum[int](1,2)
指定了类型是int (显式指定)
第二种用法,不指定类型,让编译器自行推断
Sum(1.2,2.3)
泛型结构
泛型切片
package main
type GenericSlice[T int | int32 | int64] []T
func main() {
//这里就不能省略int32
i := GenericSlice[int32]{1, 2, 3, 4}
}
泛型map
type mp[T string, V int | string] map[T]V
MP1 := mp[string, int]{"0": 1, "1": 2, "2": 3}
var MP2 = make(mp[string, int])
MP2["张三"] = 2
泛型结构体
type myStruct[T string | int] struct {
name T
age int
}
struct1 := myStruct[string]{name: "123", age: 123}
struct2 := myStruct[int]{name: 321, age: 123}
type Company[T int | string, S int | string] struct {
Name string
Id T
Stuff []S
}
泛型接口
type myinterface[T string | int] interface {
getSome() T
}
注意:
泛型不能作为一个类型的基本类型
type GenericType[T int | int32 | int64] T
虽然下列的写法是允许的,不过毫无意义而且可能会造成数值溢出的问题,虽然并不推荐
type GenericType[T int | int32 | int64] int
对泛型类型使用类型断言将会无法通过编译,泛型要解决的问题是类型无关的,如果一个问题需要根据不同类型做出不同的逻辑,那么就根本不应该使用泛型,应该使用interface{}
或者any
。
匿名结构不支持泛型
匿名结构体是不支持泛型的,如下的代码将无法通过编译
testStruct := struct[T int | string] {
Name string
Id T
}[int]{
Name: "jack",
Id: 1
}
匿名函数不支持泛型但是可以使用已存在的泛型,如闭包当中
package main
import "fmt"
func main() {
fmt.Println(getSome("1", "2"))
}
//T类型 int或者string 形参 T类型 返回T类型
func getSome[T int | string](a, b T) T {
//匿名函数使用已有的泛型
sub := func(c, d T) T {
return a + b
}
return sub(a, b)
}
方法不能拥有形参
type GenericStruct[T int | string] struct {
Name string
Id T
}
//编译不通过
func (g GenericStruct[T]) name[S int | float64](a S) S {
return a
}
但是receiver
可以拥有泛型形参
import "fmt"
func main() {
m := myStruct[string]{
Name: "小王",
Id: "123",
}
fmt.Println(m.getSome("xxx"))
}
type myStruct[T int | string] struct {
Name string
Id T
}
func (s myStruct[T]) getSome(a T) T {
return s.Id
}
类型集
在1.18以后,接口的定义变为了类型集(type set)
,含有类型集的接口又称为General interfaces
即通用接口。
类型集主要用于类型约束,不能用作类型声明,既然是集合,就会有空集,并集,交集,接下来将会讲解这三种情况。
并集
package main
func main() {
}
type SignedInt interface {
int8 | int16 | int32 | int64 | int
}
交集
非空接口的类型集是其所有元素的类型集的交集
如果一个接口包含多个非空类型集,那么该接口就是这些类型集的交集,例子如下
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type Integer interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
//Number是SignedInt和Integer两个类型集的交集
type Number interface {
SignedInt
Integer
}
//
func Do[T Number](n T) T {
return n
}
Do[int](2)//两个类型集都有int类型 所以编译可以通过过
DO[uint](2) //无法通过编译//SignedInt不含有unit
空集
空集就是没有交集,例子如下,下面例子中的Integer
就是一个类型空集。
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnsignedInt interface {
uint8 | uint16 | uint | uint32 | uint64
}
type Integer interface {
SignedInt
UnsignedInt
}
//因为无符号整数和有符号整数两个肯定没有交集,所以交集就是个空集,下方例子中不管传什么类型都无法通过编译。
Do[Integer](1)
Do[Integer](-100)
空接口
package main
func main() {
Do[string]("1")
Do[int](2)
}
// 空接口
func Do[T interface{}](a T) T {
return a
}
类型
静态强类型
Go是一个静态强类型语言,静态指的是Go所有变量的类型早在编译期间就已经确定了,在程序的生命周期都不会再发生改变,尽管Go中的短变量声明有点类似动态语言的写法,但其变量类型是由编译器自行推断的,最根本的区别在于它的类型一旦推断出来后不会再发生变化,动态语言则完全相反。所以下面的代码完全无法通过编译,因为a是int
类型的变量,不能赋值字符串。
func main() {
var a int = 64
a = "64"
fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}
类型后置
定义函数类型
f func(func(int,int)int,int)func(int,int)int
//f的类型 func 接受两个形参 一个形参是func接受两个int类型返回一个值int类型 第二个形参是int类型 整个func的返回值是一个func接受两个int类型返回一个值int类型
类型声明
type MyInt int64
type MyFloat64 float64
type MyMap map[string]int
// 可以通过编译,但是不建议使用,这会覆盖原有的类型
type int int64
通过类型声明的类型都是新类型,不同的类型无法进行运算,即便基础类型是相同的。
ype MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)//invalid operation: f1 + f (mismatched types MyFloat64 and float64)
类型别名
仅仅只是一个别名,并不是创建了一个新的类型
type Int = int
两者是都是同一个类型,仅仅叫的名字不同,所以也就可以进行运算
内置类型any
就是interface{}
的类型别名,两者完全等价,仅仅叫法不一样。
类型转换
在Go中,只存在显式的类型转换,不存在隐式类型转换
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)
即便两个类型可以相互代表,类型转换的结果也不总是正确的,看下面的一个例子:
var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))
//1 0
num1
被正确的转换为了int32
类型,但是num2
并没有。这是一个典型的数值溢出问题,int32
能够表示31位整数,int8
只能表示7位整数,高精度整数在向低精度整数转换时会抛弃高位保留低位,因此num1
的转换结果就是0。在数字的类型转换中,通常建议小转大,而不建议大转小
在使用类型转换时,对于一些类型需要避免歧义,例子如下:
*Point(p) // 等价于 *(Point(p))
(*Point)(p) // 将p转换为类型 *Point
<-chan int(c) // 等价于 <-(chan int(c))
(<-chan int)(c) // 将c转换为类型 <-chan int
(func())(x) // 将x转换为类型 func()
(func() int)(x) // 将x转换为类型 func() int
类型断言
var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
fmt.Println(intVal)
} else {
fmt.Println("error type")
}
类型判断
在Go中,switch
语句还支持一种特殊的写法,通过这种写法可以根据不同的case
做出不同的逻辑处理,使用的前提是入参必须是接口类型,示例如下:
var a interface{} = 2
switch a.(type) {
case int: fmt.Println("int")
case float64: fmt.Println("float")
case string: fmt.Println("string")
}
通过unsafe
包下提供的操作,可以绕过Go的类型系统,就可以做到原本无法通过编译的类型转换操作。