了解GO编程语言中的反思和接口
#go #接口

go是一种静态型的简约编程语言,其执行方法基于以下概念:程序是要编译和执行程序编程的指令的集合。结果,当编写大多数程序员简单,快速且熟悉的服务器端应用程序时,它做得很好。

本文将研究GO提供的反射和接口概念作为其功能集合的一部分。 反射使我们具有动态检查和了解任意对象的类型以及有关其内在结构的信息的灵活性。 接口在其自身动态上有助于识别,抽象和定义不同数据类型可以共享的行为模式。

GO的反射是什么?

go使用静态键入,这意味着每个变量在构建时间(称为静态类型)中精确地具有一种固定类型。有了很好的说明,然后就关心如何处理和与实施代码时尚不存在但可能存在的数据类型进行交互。

以最简单的形式,反射是程序在执行过程中检查/检查其自身的值和变量并确定其类型的能力。反射使我们能够检查和操纵未知结构的一种熟悉的场景是解码JSON数据。另一种情况是,当使用不实现常见接口并因此具有特殊或未知行为的数据类型时。

反思是围绕三个概念构建的:类型种类类型可以是struct,int,string,slice,map或任何其他原始类型。如果定义了名为foo的结构,则类型为 struct, foo 。用于在GO中实现反射的类型和功能位于标准库的反射软件包中。让我们看一下反射软件包中定义的一些有用的功能:

// go
package main

import (
 "fmt"
 "reflect"
)

type Foo struct {
  addr string
  dip  Dip
}

type Dip struct {
  name string
  age  int
}

var data = Foo{
  dip: Dip{name: "Alice", age: 10},
  addr: "alton street",
}

func main() {
 // Inspect the reflection Type of a variable
  rt := reflect.TypeOf(data)
  fmt.Println(rt)

 // Inspect the reflection Value of a variable
  rv := reflect.ValueOf(data)
  fmt.Println(rv)

 // Alternatively
  rt = rv.Type()
  fmt.Println(rt)

 // Inspect the reflection Kind of a variable
  rk := rv.Kind()
  fmt.Println(rk)

 // Inspect the reflection Name of a variable
  rn := rt.Name()
  fmt.Println(rn)

 // Traverse the fields of a struct.
 if rt.Kind() == reflect.Struct {
 for i := 0; i < rt.NumField(); i++ {
      fv := rv.Field(i)
      fmt.Println(fv)
    }
  }

 // Inspect the underlying contained type in a variable
  rt = reflect.TypeOf([2]int {1, 2})
  ct = ct.Elem()
  fmt.Println(ct)
}

Save this code

输出:

// terminal
main.Foo
{alton street {Alice 10}}
main.Foo
struct
Foo
alton street
{Alice 10}
int

在上面的片段中,我们将两个结构(FooDip)定义为与reflection软件包一起使用的样品结构。 reflect.TypeOf函数返回reflect.Type,该方法在其类型上定义的方法提供了有关传递的变量类型的其他信息。reflect.ValueOf函数返回reflect.Value,允许反射读取或写入值。

>

除了阅读外,我们还可以使用reflection软件包修改/编写结构的值。要使用reflect.ValueOf函数修改值,我们需要将指针传递给变量,然后调用Elem函数,该函数将在指针地址返回值。

// go
package main

import (
 "fmt"
 "reflect"
)

type Pieces struct {
  Id int
  Content string
  Rating float64
}

func main() {
  data := Pieces{1, "I come in peace, comrade!", 4.1}
  fmt.Printf("Original data: %+v\n", data)
  mutData := reflect.ValueOf(&data).Elem()

 for i := 0; i < mutData.NumField(); i++ {
 if mutData.Field(i).Kind() == reflect.Int {
      mutData.Field(i).SetInt(12)
    } else if mutData.Field(i).Kind() == reflect.String {
      mutData.Field(i).SetString("No, I live for violence, comrade!")
    } else if mutData.Field(i).Kind() == reflect.Float64 {
      mutData.Field(i).SetFloat(5.5)
    }
  }

  fmt.Printf("Mutated data: %+v\n", data)
}

Save this code

输出

// terminal
Original data: {Id:1 Content:I come in peace, comrade! Rating:4.1}
Mutated data: {Id:12 Content:No, I live for violence, comrade! Rating:5.5}

在上面的片段中,请注意使用SetIntSetStringSetFloat功能来编写结构的新值。 reflect软件包定义了更多这些功能,以更改原始数据类型的价值。查找有关reflect package的更多信息。请注意,始终是一个安全的练习,可以通过reflect.Value.CanSet函数检查结构的值。

类型方法和界面的简介

类型方法

当将函数附加到特定数据类型(接收方参数)时,我们将此视为GO中的类型方法。接收器参数使该方法需要访问基础数据类型,以将其读取,写入它或两者。

我们可以使用以下GO语法来定义类型方法:

// A copy of the receiver argument
func (p Pieces) refChange() { } // <-- 1

// a pointer to the receiver argument
func (p *Pieces) refChange() { } // <-- 2

用第一个语法定义类型方法意味着我们将接收器参数值的副本传递给refChange函数。在第二个定义中,我们定义并将指针传递给了接收器参数,而不是传递接收器参数的副本。该定义使我们可以直接访问refChange函数中的接收器参数,这意味着我们可以坚持对接收者参数的更改。

让我们在Pieces struct上实现一种方法,以使用reflect软件包更改结构的值:

// go
...
func (p *Pieces) refChange(id int64, content string, rating float64) {
  mutData := reflect.ValueOf(p).Elem()
 for i := 0; i < mutData.NumField(); i++ {
 if mutData.Field(i).Kind() == reflect.Int {
      mutData.Field(i).SetInt(id)
    } else if mutData.Field(i).Kind() == reflect.String {
      mutData.Field(i).SetString(content)
    } else if mutData.Field(i).Kind() == reflect.Float64 {
      mutData.Field(i).SetFloat(rating)
      mutData.Field(i).CanSet()
    }
  }
}

func main() {
  data := &Pieces{1, "I come in peace, comrade!", 4.1}
  fmt.Printf("Original data: %+v\n", data)

  data.refChange(100, "No, peace is not an option comrade!", 4.7)
  fmt.Printf("Mutated data: %+v\n", data)
}

Save this code

输出

// terminal
Original data: {Id:1 Content:I come in peace, comrade! Rating:4.1}
Mutated data: {Id:100 Content:No, peace is not an option comrade! Rating:4.7}

这是对GO结构的类型方法的简单介绍。 Learn more about the type method in the Go documentation.

接口

在GO中,接口是一种使用一组方法签名实现的行为的机制。接口类型通过定义一组类型方法来描述其他类型的行为期望,这些方法需要由这些其他类型实现,然后才声称支持行为。从本质上讲,接口可与与给定数据类型相关联的类型方法使用;尽管我们可以在GO中使用任何数据类型,但通常是结构的。因此,在GO类型满足接口类型之前,它需要实现由接口类型定义的所有类型方法。

...
type Blog interface {
  ReadContent(string) (string, int)
  GetRating() (float64)
}
...

Save this code

上面的片段定义了一个带有两个函数签名的接口类型,即ReadContentGetRating。对于类型,可以实现此接口,这意味着它具有定义的ReadContentGetRating类型方法。让我们定义Pieces结构的这些类型方法:

...
func (p *Pieces) ReadContent(input string) (string, int) {
 if input != "" {
    p.Content = input
  }
 return p.Content, utf8.RuneCountInString(p.Content)
}

func (p *Pieces) GetRating() (float64) {
 return p.Rating
}
...

Save this code

在上面的片段中,一旦我们实现了Pieces数据类型的接口功能,则该接口将自动满足该数据类型。为了使这些内容有意义,我们想说我们要在我们的博客上发布任何帖子,但我们希望该帖子可读并且可以评估。借助接口类型,我们可以在Publish函数上放置某种边界:

...
func Publish(post Blog) {
 if r := post.GetRating(); r != 0 {
    c, rc := post.ReadContent("")
    fmt.Printf("New post: %s\nWord count:%d\tWith rating: %.1f\n", c, rc, r)
  }
}
...

Save this code

输出:

// terminal
New post: No, peace is not an option comrade!
Word count:35   With rating: 4.7

接口是抽象类型,概述了一组方法必须为另一种类型实现,然后才能将其视为接口的实例。换句话说,界面既由一种类型和方法组成。

一个空界面被定义为仅interface{}。由于一个空接口没有方法,因此它可以并且已经由每个数据类型实现。例如,将其元素定义为一种空接口的切片意味着它可以在本身中存储任何东西。

// go
s := []interface{}{4, "string", 2.5, new(Pieces), true} // [4 string 2.5 0x140000ae000 true]

Save this code

建议很少使用空界面,尤其是在存储数据序列以避免在编译时间中易于跟踪的错误时。

让我们简要看一下从安装时运送的标准库中的三个常用接口。这三个接口是sort.Interfaceio.Writerio.Reader接口。让我们简要说明以下每个:

  1. sort.Interfacesort.Interface定义了自定义实现的三种必要方法,以对自定义数据进行分类。 sort软件包包含以下定义的sort.Interface接口:
// go
type Interface interface {
  Len() int
  Less(i, j int) bool
  Swap(i, j int)
}

Save this code

一旦我们实现了存储在切片中的自定义数据的sort.Interface,以上三种方法允许我们根据自定义需求和数据对切片进行排序。

  • Len函数返回要排序的切片的实际长度。
  • Less函数包含如何比较和分类元素的实现;此实现将是开发人员需求的定制,但必须返回索引ij之间比较的布尔值。
  • Swap根据Less的结果对切片中元素进行实际交换。这是分类算法工作所必需的。

    2。 io.Writer:此界面代表go中文件I/O的基础的写作部分。写作对文件,网络或任何可写过程的操作涉及该写入对象(数据类型)的io.Writer的实现。接口定义是:

// go
type Writer interface {
  Write(p []byte) (n int, err error)
}

Save this code

上面的接口定义仅需要一个函数签名来满足Writer接口。 Write函数将其参数作为一个字节切片,其中包含我们要写入的数据,即文件,网络或任何可写的对象,并返回两个命名值nerr。这些值代表编写的字节数和error变量。

  1. io.Reader:此接口表示文件I/O的读数部分,从文件,网络或任何可读过程中读取的操作中。 Reader接口仅定义一个函数签名,就像io.Writer一样。让我们看接口定义:
// go
type Reader interface {
  Read(p []byte) (n int, err error)
}

Save this code

上面的定义将其参数作为一个字节切片,我们将填充从文件,网络或任何可读对象中读取的数据。我们填充字节切片的数据长度达到其长度。返回值表示字节读的数量和error变量。

GO接口的用例

接口的概念主要用于不同的GO软件包和可执行程序,以实现灵活性而不会影响程序的性能。以下示例将探索使用sort.InterfacePieces结构类型进行排序:

// go
...
type PSlice []Pieces

func (ps PSlice) Len() int {
 return len(ps)
}

func (ps PSlice) Less(i, j int) bool {
 return ps[i].Id < ps[j].Id
}

func (ps PSlice) Swap(i, j int) {
  ps[i], ps[j] = ps[j], ps[i]
}

...

func main() {
  posts := []Pieces {
    {7, "I come in peace, comrade!", 4.1},
    {100, "No, peace is not an option comrade!", 4.7 },
    {13, "I preach violence, comrade!", 4.5},
    {-1, "I live for violence, comrade", 5.5},
  }
  fmt.Println("Original:", posts)
  sort.Sort(PSlice(posts))
  fmt.Println("Sorted:", posts)

 // Reverse sorting automatically works
  sort.Sort(sort.Reverse(PSlice(posts)))
  fmt.Println("Reverse sort:", posts)
  ...
}

Save this code

输出

Original: [{7 I come in peace, comrade! 4.1} {100 No, peace is not an option comrade! 4.7} {13 I preach violence, comrade! 4.5} {-1 I live for violence, comrade 5.5}]

Sorted: [{-1 I live for violence, comrade 5.5} {7 I come in peace, comrade! 4.1} {13 I preach violence, comrade! 4.5} {100 No, peace is not an option comrade! 4.7}]

Reverse sort: [{100 No, peace is not an option comrade! 4.7} {13 I preach violence, comrade! 4.5} {7 I come in peace, comrade! 4.1} {-1 I live for violence, comrade 5.5}]

这是一个漫长的例子,但仅实现了我们之前分享的有关sort.Interface接口的基础概念。我们实施了LenLessSwap的类型PSlice,这基本上是Pieces的切片。使用类型方法,我们现在可以使用sort.Sort函数对切片进行排序,还可以使用sort.redverse函数在切片上执行反向排序。 PSlice(posts)表达式将使用方法LenLessSwap[]Pieces切片调整为PSlice类型。这是可能的,因为[]Pieces具有PSlice的适当类型签名。

结论

在本教程中,我们介绍了GO中的反射和接口。例如,反射提供了一种更优雅的方法,可以在记录敏感数据之前擦洗敏感数据。而且,界面还可以根据数据类型预期的行为来实现功能。现在,我们可以通过将一个接口与另一个接口相结合以形成新界面来扩展这些概念以构建复杂的接口。 ReadCloser接口是组合两个接口ReaderCloser的典型示例。这些概念的可扩展性远远超出了本教程,因此我鼓励您更多地探索这些概念。有关何时使用的更多信息,请查看this comparison guide