golang基础 第3章 (struct,函数,方法,接口,泛型,类型集,类型)

结构体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就是接收者的类型,接收者就类似于其他语言中的thisself,只不过在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的类型系统,就可以做到原本无法通过编译的类型转换操作。

  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
golang中,泛型是一种通用类型的编程方式,可以在代码中使用不特定具体类型来实现通用性。然而,目前的golang版本(截至2021年)并不直接支持泛型。 虽然golang没有原生的泛型支持,但可以通过接口方法来实现类似的效果。在接口的定义中,可以使用空接口类型(interface{})作为参数或返回类型。空接口类型可以接收任意类型的值。 在使用空接口类型作为接口方法的参数时,可以将任意类型值传递给该方法。在方法内部,可以使用类型断言将空接口类型转换成具体的类型,从而实现对不同类型的值的处理。这样可以实现一种类似于泛型的效果,通过接口方法来处理不同类型的值。 示例代码如下: ```go package main import "fmt" type Generic interface { Process(interface{}) interface{} } type StringContainer struct{} func (sc StringContainer) Process(item interface{}) interface{} { str, ok := item.(string) if !ok { return "error: not a string" } return "string: " + str } type IntContainer struct{} func (ic IntContainer) Process(item interface{}) interface{} { num, ok := item.(int) if !ok { return "error: not an int" } return "int: " + fmt.Sprintf("%d", num) } func main() { strContainer := StringContainer{} intContainer := IntContainer{} strResult := strContainer.Process("hello") intResult := intContainer.Process(123) fmt.Println(strResult) // output: string: hello fmt.Println(intResult) // output: int: 123 } ``` 在上述示例中,我们定义了一个`Generic`接口,该接口包含了一个`Process`方法,该方法接收一个空接口类型参数并返回一个空接口类型结果。然后我们定义了`StringContainer`和`IntContainer`两个结构体,并为它们实现了`Process`方法。在`Process`方法中,我们使用类型断言将参数转换为具体的类型,然后进行相应的处理。最后在`main`函数中,我们创建了`StringContainer`和`IntContainer`的实例,并调用其`Process`方法来处理不同类型的值。 通过接口方法的利用,我们可以实现类似于泛型的效果。当然,这种方式并不是完全等同于原生的泛型特性,而是一种在golang中模拟实现泛型方法

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值