Go基础函数和面向接口编程

Posted Ricky_0528

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go基础函数和面向接口编程相关的知识,希望对你有一定的参考价值。

文章目录

一、函数

1. 函数的基本形式

// 函数定义:a,b是形参
func add(a int, b int)  
	a = a + b 

var x, y int = 3, 6
add(x, y) // 函数调用:x,y是实参
  • 形参是函数内部的局部变量,实参的值会拷贝给形参
  • 函数定义时的第一个的大括号不能另起一行
  • 形参可以有0个或多个,支持使用可边长参数
  • 参数类型相同时可以只写一次,比如add(a,b int)
  • 在函数内部修改形参的值,实参的值不受影响
  • 如果想通过函数修改实参,就需要传递指针类型
    func change(a, b *int)  
        *a = *a + *b
        *b = 888
    
    var x, y int = 3, 6
    change(&x, &y)
    

slice、map、channel都是引用类型,它们作为函数参数时其实跟普通struct没什么区别,都是对struct内部的各个字段做一次拷贝传到函数内部

package main

import "fmt"

// slice作为参数,实际上是把slice的arrayPointer、len、cap拷贝了一份传进来
func sliceChange(arr []int)  
	arr[0] = 1 // 实际是修改底层数据里的首元素
	arr = append(arr, 1) // arr的len和cap发生了变化,不会影响实参


func main() 
	arr := []int8
	sliceChange(arr)
	fmt.Println(arr[0])   // 1,数组元素发生改变
	fmt.Println(len(arr)) // 1,实际的长度没有改变

关于函数返回值

  • 可以返回0个或多个参数
  • 可以在func行直接声明要返回的变量
  • return后面的语句不会执行
  • 无返回参数时return可以不写
// 返回变量c已经声明好了,在函数中可以直接使用
func returnf(a, b int) (c int) 
    a = a + b
    c = a // 直接使用c
    return // 由于函数要求有返回值,即使给c赋过值了,也需要显式写return

不定长参数实际上是slice类型

// other为不定长参数可传递任意多个参数,a是必须传递的参数
func args(a int, other ...int) int  
    sum := a
    // 直接当作slice来使用
    for _, ele := range other 
        sum += ele
    
    fmt.Printf("len %d cap %d\\n", len(other), cap(other))
    return sum

args(1)
args(1,2,3,4)

append函数接收的就是不定长参数

arr = append(arr, 1, 2, 3)
arr = append(arr, 7)
arr = append(arr)
slice := append([]byte("hello "), "world"...) // ...自动把"world"转成byte切片,等价于[]byte("world")...
slice2 := append([]rune("hello "), []rune("world")...) // 需要显式把"world"转成rune切片

在很多场景下string都隐式的转换成了byte切片,而非rune切片,比如"a中"[1]获取到的值为228而非"中"

2. 递归函数

最经典的斐波那契数列的递归求法

func fibonacci(n int) int 
    if n == 0 || n == 1 
        return n // 凡是递归,一定要有终止条件,否则会进入无限循环
    
    return fibonacci(n-1) + fibonacci(n-2) // 递归调用自身

3. 匿名函数

函数也是一种数据类型

func functionArg1(f func(a, b int) int, b int) int  // f参数是一种函数类型
	a := 2 * b
	return f(a, b)


type foo func(a, b int) int // foo是一种函数类型
func functionArg2(f foo, b int) int  // type重命名之后,参数类型看上去简洁多了
    a := 2 * b
    return f(a, b)


type User struct 
    Name string
    bye foo // bye的类型是foo,也就是是函数类型
    hello func(name string) string // 使用匿名函数来声明struct字段的类型为函数类型


ch := make(chan func(string) string, 10)
// 使用匿名函数向管道中添加元素
ch <- func(name string) string 
	return "hello " + name

4. 闭包

闭包(Closure)是引用了自由变量的函数,自由变量将和函数一同存在,即使已经离开了创造它的环境,闭包复制的是原对象的指针

package main

import "fmt"

func sub() func() 
	i := 10
	fmt.Printf("%p\\n", &i)
	b := func() 
		fmt.Printf("i addr %p\\n", &i) // 闭包复制的是原对象的指针
		i-- // b函数内部引用了变量i
		fmt.Println(i)
	
	return b // 返回了b函数,变量i和函数b将一起存在,即使已经离开函数sub()


// 外部引用函数参数局部变量
func add(base int) func(int) int 
	return func(i int) int 
		fmt.Printf("base addr %p\\n", &base)
		base += i
		return base
	


func main() 
	b := sub()
	b()
	b()
	fmt.Println()

	tmp1 := add(10)
	fmt.Println(tmp1(1), tmp1(2))
	// 此时tmp1和tmp2不是一个实体了
	tmp2 := add(100)
	fmt.Println(tmp2(1), tmp2(2))

5. 延迟调用defer

  • defer用于注册一个延迟调用(在函数返回之前调用)
  • defer典型的应用场景是释放资源,比如关闭文件句柄,释放数据库连接等
  • 如果同一个函数里有多个defer,则后注册的先执行,相当于是一个栈
  • defer后可以跟一个func,func内部如果发生panic,会把panic暂时搁置,当把其他defer执行完之后再来执行这个
  • defer后不是跟func,而直接跟一条执行语句,则相关变量在注册defer时被拷贝或计算
func basic() 
    fmt.Println("A")
    defer fmt.Println(1) fmt.Println("B")
    // 如果同一个函数里有多个defer,则后注册的先执行
    defer fmt.Println(2)
    fmt.Println("C")

func deferExecTime() (i int) 
	i = 9
	// defer后可以跟一个func
	defer func() 
		fmt.Printf("first i=%d\\n", i) // 打印5,而非9,充分理解“defer在函数返回前执行”的含义,不是在“return语句前执行defer”
	()
	defer func(i int) 
		fmt.Printf("second i=%d\\n", i) // 打印9
	(i)
	defer fmt.Printf("third i=%d\\n", i) // 打印9,defer后不是跟func,而直接跟一条执行语句,则相关变量在注册defer时被拷贝或计算
	return 5

6. 异常处理

go语言没有try catch,它提倡直接返回error

func divide(a, b int) (int, error) 
    if b == 0 
        return -1, errors.New("divide by zero")
    
    return a / b, nil

// 函数调用方判断error是否为nil,不为nil则表示发生了错误
if res, err := divide(3, 0); err != nil 
    fmt.Println(err.Error())

Go语言定义了error这个接口,自定义的error要实现Error()方法

// 自定义error
type PathError struct 
    path string
    op string
    createTime string
    message string

// error接口要求实现Error() string方法
func (err PathError) Error() string 
	return err.createTime + ": " + err.op + " " + err.path + " " + err.message

何时会发生panic:

  • 运行时错误会导致panic,比如数组越界、除0
  • 程序主动调用panic(error)

panic会执行什么:

  • 逆序执行当前goroutine的defer链(recover从这里介入)
  • 打印错误信息和调用堆栈
  • 调用exit(2)结束整个进程
func soo() 
	fmt.Println("enter soo")
	
	// 去掉这个defer试试,看看panic的流程,把这个defer放到soo函数末尾试试
	defer func() 
		// recover必须在defer中才能生效
		if err := recover(); err != nil 
			fmt.Printf("soo panic:%s\\n", err)
		
	()
	fmt.Println("regist recover")

	defer fmt.Println("hello")
	defer func() 
		n := 0
		_ = 3 / n // 除0异常,发生panic,下一行的defer没有注册成功
		defer fmt.Println("how are you")
	()

二、面向接口编程

1. 接口的基本概念

接口是一组行为规范的集合

// 定义接口,通常接口名以er结尾
type Transporter interface 
    // 接口里面只定义方法,不定义变量
    move(src string, dest string) (int, error) // 方法名 (参数列表) 返回值列表
    whistle(int) int // 参数列表和返回值列表里的变量名可以省略

只要结构体拥有接口里声明的所有方法,就称该结构体“实现了接口”,一个struct可以同时实现多个接口

// 定义结构体时无需要显式声明它要实现什么接口
type Car struct 
    price int


func (car Car) move(src string, dest string) (int, error) 
    return car.price, nil

func (car Car) whistle(n int) int 
    return n

接口值有两部分组成, 一个指向该接口的具体类型的指针和另外一个指向该具体类型真实数据的指针

car := Car"宝马", 100
var transporter Transporter
transporter = car

2. 接口的使用

func transport(src, dest string, transporter Transporter) error 
	 _,err := transporter.move(src, dest)
	return err

var car Car // Car实现了Transporter接口
var ship Shiper	// Shiper实现了Transporter接口
transport("北京", "天津", car)
transport("北京", "天津", ship)

3. 接口的赋值

// 方法接收者是值
func (car Car) whistle(n int) int 

// 方法接收者用指针,则实现接口的是指针类型
func (ship *Shiper) whistle(n int) int 

car := Car
ship := Shiper
var transporter Transporter
transporter = car 
transporter = &car // 值实现的方法,默认指针同样也实现了
transporter = &ship // 但指针实现的方法,值是没有实现的

4. 接口嵌入

type Transporter interface 
	whistle(int) int

type Steamer interface 
    Transporter // 接口嵌入,相当于Transporter接口定义的行为集合是Steamer的子集
    displacement() int

5. 空接口

空接口类型用interface表示,注意有

var i interface 

空接口没有定义任何方法,因此任意类型都实现了空接口

var a int = 5
i = a
func square(x interface) // 该函数可以接收任意数据类型

注意:slice的元素、map的key和value都可以是空接口类型,map中的key可以是任意能够用==操作符比较的类型,不能是函数、map、切片,以及包含上述3中类型成员变量的的struct,map的value可以是任意类型

6. 类型断言

// 若断言成功,则ok为true,v是具体的类型
if v, ok := i.(int); ok 
	fmt.Printf("i是int类型,其值为%d\\n", v)
 else 
	fmt.Println("i不是int类型")

当要判断的类型比较多时,就需要写很多if-else,更好的方法是使用switch i.(type),这也是标准的写法

switch v := i.(type)  // 隐式地在每个case中声明了一个变量v
case int:  // v已被转为int类型
	fmt.Printf("ele is int, value is %d\\n", v)
	// 在 Type Switch 语句的 case 子句中不能使用fallthrough
case float64: // v已被转为float64类型
	fmt.Printf("ele is float64, value is %f\\n", v)
case int8, int32, byte: // 如果case后面跟多种type,则v还是interface类型
	fmt.Printf("ele is %T, value is %d\\n", v, v)

7. 面向接口编程

电商推荐流程

为每一个步骤定义一个接口

type Recaller interface 
    Recall(n int) []*common.Product // 生成一批推荐候选集

type Sorter interface 
    Sort([]*common.Product) []*common.Product // 传入一批商品,返回排序之后的商品

type Filter interface 
    Filter([]*common.Product) []*common.Product // 传入一批商品,返回过滤之后的商品

type Recommender struct 
    Recallers []recall.Recaller
    Sorter sort.Sorter
    Filters []filter.Filter

使用纯接口编写推荐主流程

func (rec *Recommender) Rec() []*common.Product 
	RecallMap := make(map[int]*common.Product, 100)
	// 顺序执行多路召回
	for _, recaller := range rec.Recallers 
		products := recaller.Recall(10) // 统一设置每路最多召回10个商品
		for _, product := range products 
			RecallMap[product.Id] = product // 把多路召回的结果放到map里,按Id进行排重
		
	
	// 把map转成slice
	RecallSlice := make([]*common.Product, 0, len(RecallMap))
	for _, product := range RecallMap 
		RecallSlice = append(RecallSlice, product)
	
	SortedResult := rec.Sorter.Sort(RecallSlice) // 对召回的结果进行排序
	// 顺序执行多种过滤规则
	FilteredResult := SortedResult
	for _, filter := range rec.Filters 
		FilteredResult = filter.Filter(FilteredResult)
	
	return FilteredResult

面向接口编程,在框架层面全是接口。具体的实现由不同的开发者去完成,每种实现单独放到一个go文件里,大家的代码互不干扰。通过配置选择采用哪种实现,也方便进行效果对比

以上是关于Go基础函数和面向接口编程的主要内容,如果未能解决你的问题,请参考以下文章

Go基础函数和面向接口编程

Go语言学习——结构体构造函数方法和接收者给自定义类型添加方法

Go 语言入门很简单 -- 12. Go 方法 #私藏项目实操分享#

go——方法

《Go学习笔记 . 雨痕》方法

go语音之进阶篇方法面向过程和对象函数的区别