盘点一下Go语言的反射用法

120 阅读16分钟

1. 什么是反射

反射可以认为是程序在运行时的一种能力,反射可以在程序运行时访问、检测和修改它本身状态,比如在程序运行时可以检查变量的类型和值,调用它们的方法,甚至修改它们的值。使用反射可以增加程序的灵活性,简单来说,反射就是程序在运行时能够检测自身和修改自身的一种能力

2. Go语言反射

对于很多的高级语言都实现了反射,像Java,Python。在Go语言中,反射在Go语言内置的reflect包下实现。Go语言中的反射建立在Go的类型系统之上,并且与接口密切相关。通过前面的学习我们知道Go语言的空接口包含类型(Type)和值(Value)两个部分,在反射里,也要用到类型(Type)和值(Value)
reflect包中定义了reflect.Typereflect.Value,正好对应我们前面所说的TypeValue。要注意的是reflect.Type是一个接口而reflect.Value是一个具体的结构体。在reflect.Type接口中定义了很多跟类型相关的方法,而reflect.Value则是绑定了很多跟值相关的方法

2.1 reflect.TypeOf()

由于reflect.Type是一个接口,所以只有当某个类型实现了这个接口,我们才能获取到它的类型,同时,在reflect包内,类型描述符是未导出类型,所以我们只能通过reflect.TypeOf()方法获取reflect.Type类型的值。
我们首先看一个例子,看下reflect.TypeOf()的常用用法

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name string
    Age  int
}

func main() {
    var num int64 = 100
    t1 := reflect.TypeOf(num)
    fmt.Println(t1.String())

    st := Student{
       Name: "zhangsan",
       Age:  18,
    }
    t2 := reflect.TypeOf(st)
    fmt.Println(t2.String())
}

代码输出:

int64
main.Student

可以看到对于基础类型和struct类型通过调用reflect.TypeOf()都打印出了对应的类型信息。注意reflect.TypeOf返回的是一个reflect.Type接口类型,我们通过调用这个接口的String()方法,得到最终的字符串信息
学习过interface的章节后,我们知道一个具体的数据类型是可以赋值给一个interface类型的,反过来则不行,要用到interface的断言。在一个interface赋值之后,其实是对应了两个类型,一个是静态类型,就是在程序编译期就确定的类型,interface的静态类型就是接口interface,同时当interface赋值之后,他还有一个动态类型,就是被赋值的那个数据的具体类型,假设在上例中,我们将st赋值个一个空interface,那么这个interface的动态类型就是Student
对一个数据对象进行反射操作,其实是首先将具体对象类型转化为一个interface类型,然后再将interface类型转化为reflect包下的反射类型,反射类型里的类型信息和值信息其实就是对应着这个中间类型interface的类型和值 反射1 reflect.TypeOf()方法获取的就是这个interface{}中的类型部分

2.2 reflect.ValueOf()

同理,reflect.ValueOf()方法自然就是获取接口中的值部分,reflect.ValueOf()的返回值其实就是一个reflect.Value结构

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name string
    Age  int
}

func main() {
    var num int64 = 100
    v1 := reflect.ValueOf(num)
    fmt.Println(v1)
    fmt.Println(v1.String())

    st := Student{
       Name: "zhangsan",
       Age:  18,
    }
    v2 := reflect.ValueOf(st)
    fmt.Println(v2)
    fmt.Println(v2.String())
}

程序输出:

100
<int64 Value>
{zhangsan 18}
<main.Student Value>

注意到这里fmt.Println(v1)fmt.Println(v1.String())打印的不一样,上面说了reflect.ValueOf()的返回值就是一个reflect.Value结构,但是fmt.Println(v1)缺打印出了具体的值,这是因为fmt.Println的参数是一个接口类型,在执行过程中有一些类型转换,对reflect.Value结构做了特殊处理

2.3 Go语言数据种类

在Go语言中常用的数据类型有26种,以枚举的方式定义在src/reflect/type.go文件中 反射2 这些类型中包含intbool之类的基础数据类型,也包含StructArrayMap等复合类型,有了这些类型,我们用type struct自定义的任何类型都可以由他们组合完成
看个type struct定义的数据类型使用反射的例子

package main

import (
    "fmt"
    "reflect"
)

type WrapInt int

func main() {
    var num1 int = 100
    var num2 WrapInt = 1000

    num1 = int(num2) // 不同类型的type赋值,这里要强转

    typeNum1 := reflect.TypeOf(num1)
    fmt.Printf("type of num1 is %s\n", typeNum1.String())

    typeNum2 := reflect.TypeOf(num2)
    fmt.Printf("type of num2 is %s\n", typeNum2.String())

    fmt.Printf("kind of num1 is %v\n", typeNum1.Kind())
    fmt.Printf("kind of num2 is %v\n", typeNum2.Kind())
}

程序输出

type of num1 is int
type of num2 is main.WrapInt
kind of num1 is int
kind of num2 is int

通过WrapInt的定义可以看到,WrapInt其实就是用typeint取了个别名,二者底层其实都是int类型,但是通过reflect.TypeOf获取到各自的type其实是不一样的,不同type之间的变量赋值是需要类型强制转换的,但是深层次的去分析type的种类,即Kind确是一样的

3. 反射使用

3.1 值对象

reflect包下跟值对象相关的常用函数或方法

函数/方法说明
reflect.TypeOf()获取某个对象的反射类型实现(reflect.Type)
reflect.ValueOf()获取某个对象的反射值对象(reflect.Value)
reflect.Value.NumField()获取结构体的反射值对象中的字段个数,只对结构体类型有效
reflect.Value.Field(i)获取结构体的反射值对象中的第i个字段,只对结构体类型有效
reflect.Kind()从反射值对象中获取该值的种类
reflect.Value.MapKeys()对map的每个键的reflect.Value对象组成的一个切片
reflect.Value.MapIndex(i)根据map的某个键的reflect.Value对象,返回值的reflect.Value对象
reflect.Value.Len()对切片或数组的反射对象求切片或数组的长度
reflect.Value.Index(i)返回切片或数组第i个元素的reflect.Value值
reflect.Int()/reflect.Uint()/reflect.String()/reflect.Bool()从反射的值对象中取出对应值,注意reflect.Int()/reflect.Uint()方法对种类做了合并处理,它们只返回相应的最大范围的类型,Int()返回Int64类型,Uint()返回Uint64类型

3.1.1 获取struct反射值

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {

    st := Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 95.5,
    }
    v := reflect.ValueOf(st)
    fmt.Printf("the field num of Student is %d\n", v.NumField())
    fmt.Printf("field1 type is %v, value is %s\n", v.Field(0).Type().Name(), v.Field(0).String())
    fmt.Printf("field2 type is %v, value is %d\n", v.Field(1).Type().Name(), v.Field(1).Int())
    fmt.Printf("field2 type is %v, value is %f\n", v.Field(2).Type().Name(), v.Field(2).Float())
}

程序输出

the field num of Student is 3
field1 type is string, value is zhangsan
field2 type is int, value is 18
field2 type is float64, value is 95.500000

v := reflect.ValueOf(st)v是一个Student类型的反射值对象,通过v.NumField()可以得出Student类型的字段个数,然后v.Field(i).Type().Name()打印出各个字段值的类型,v.Field(i)打印出各个字段值
注意NumField()Field()方法只有原对象是结构体时才能调用,否则会panic

3.1.2 获取map反射值

package main

import (
    "fmt"
    "reflect"
)

func main() {
    m := map[int]uint32{
       1100,
       2200,
    }
    v := reflect.ValueOf(m)
    for _, k := range v.MapKeys() {
       field := v.MapIndex(k)
       fmt.Printf("key type is %v, key = %d; value type is %v, value = %d\n", k.Type().Name(), k.Int(), field.Type().Name(), field.Uint())
    }
}

程序输出

key type is int, key = 1; value type is uint32, value = 100
key type is int, key = 2; value type is uint32, value = 200

v := reflect.ValueOf(m)map类型的对象m进行反射,通过v.MapKeys()的到m中所有keyreflect.Value对象k,然后通过v.MapIndex(k)的到对应key反射值对象的value反射值对象,然后通过reflect.ValueType().Name()方法获取mapkeyvalue的类型,然后打印出对应值

3.1.3 获取slice反射值


import (
    "fmt"
    "reflect"
)

func main() {
    slice := []int{123}
    v1 := reflect.ValueOf(slice)
    for i := 0i < v1.Len(); i++ {
       elem := v1.Index(i)
       fmt.Printf("%v ", elem.Interface())
    }

    fmt.Println()

    nums := [3]int{456}
    v2 := reflect.ValueOf(nums)
    for i := 0i < v2.Len(); i++ {
       elem := v2.Index(i)
       fmt.Printf("%v ", elem.Interface())
    }
}

程序输出

1 2 3 
4 5 6 

v1v2分别是切片和数组的反射值对象,通过Len()获取到数组或切片中的元素个数,然后通过v.Index(i)获取对应元素的reflect.value对象,打印出其值 注意Len()Index(i)方法只能在原对象是切片,数组或字符串时才能调用,其他类型会panic

3.2 类型对象

reflect包下跟类型相关的常用函数或方法

函数/方法说明
reflect.Value.NumField()获取结构体的反射值对象中的字段个数,只对结构体类型有效
reflect.Value.Field(i)获取结构体的反射值对象中的第i个字段,只对结构体类型有效
reflect.Value.Elem()根据指针获取对应的具体类型
reflect.Value.NumIn()获取函数反射类型的参数个数
reflect.Value.In(i)获取函数反射类型的第i个参数
reflect.Value.NumOut()获取函数反射类型的返回值个数
reflect.Value.Out(i)获取函数反射类型的第i个返回值
reflect.Value.NumMethod()获取struct上绑定的方法个数
reflect.Value.Method(i)获取struct上绑定的第i个方法

3.2.1 struct反射类型

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    st := Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    t := reflect.TypeOf(st)
    fmt.Println(t.Name())
    fmt.Println(t.Kind())
    fmt.Println(t.NumField())
    for i := 0i < t.NumField(); i++ {
       fmt.Printf("field1 name is %s, field1 type is %s\n", t.Field(i).Name, t.Field(i).Type.String())
    }
}

程序输出

Student
struct
3
field1 name is Name, field1 type is string
field1 name is Age, field1 type is int
field1 name is Score, field1 type is float64

通过reflect.TypeName()方法可以获取对应的Type类型,Kind()方法获取底层的数据种类,即kind,跟reflect.Value一样,reflect.Type也提供了NumField()方法用于获取结构体对象中的字段个数,通过t.Field(i).Name可以获取对应字段的名字
同样,Field(i)NumField()也只能对结构体反射使用

3.2.2 指针反射类型

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    t := reflect.TypeOf(st)

    fmt.Println(t.Kind())
    fmt.Println(t.Elem().Name())     // 这里一定要加Elem(),根据指针获取到具体类型后,才能或者具体的type名
    fmt.Println(t.Elem().NumField()) // 这里一定要加Elem(),根据指针获取到具体类型后,才能字段个数
    for i := 0i < t.Elem().NumField(); i++ {
       fmt.Printf("field1 name is %s, field1 type is %s\n", t.Elem().Field(i).Name, t.Elem().Field(i).Type.String())
    }

}

程序输出

ptr
Student
3
field1 name is Name, field1 type is string
field1 name is Age, field1 type is int
field1 name is Score, field1 type is float64

可以看到,跟上面直接获取struct有一点点小小的区别,那就是fmt.Println(t.Kind())打印出的是一个ptr指针类型,而不再是struct类型,正是因为这里是一个ptr,所以我们不能直接在这个ptr上调用.Name()以及其他的.NumField()之类的方法,要根据ptr.Elem()获取到具体类型之后,才能用这些方法,否则程序就会报panic,这点一定要注意

3.2.3 函数反射类型

package main

import (
    "fmt"
    "reflect"
)

func Add(num1, num2 int) (interror) {
    return num1 + num2, nil
}

func main() {

    fmt.Println("input:")
    t := reflect.TypeOf(Add)
    for i := 0; i < t.NumIn(); i++ {
       tIn := t.In(i)
       fmt.Print(tIn.Name())
       fmt.Printf(" ")
    }
    fmt.Printf("\n--------------------------------")

    fmt.Println("output:")
    for i := 0; i < t.NumOut(); i++ {
       tOut := t.Out(i)
       fmt.Print(tOut.Name())
       fmt.Print(" ")
    }

}

程序输出

input:
int int 
--------------------------------
output:
int error 

t := reflect.TypeOf(Add)获取到Add函数的type类型,然后通过NumIn()方法获得Add函数的参数个数,依次打印出参数的类型。通过NumOut()方法获得Add函数的返回值个数,依次打印出返回值的类型

3.2.4 反射获取struct方法

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func (s *Student) GetName() string {
    return s.Name
}

func (s *Student) SetName(name string) {
    s.Name = name
}

func (s *Student) GetAge() int {
    return s.Age
}

func (s *Student) SetAge(age int) {
    s.Age = age
}

func (s *Student) GetScore() float64 {
    return s.Score
}

func (s *Student) SetScore(score float64) {
    s.Score = score
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    t := reflect.TypeOf(st)

    for i := 0; i < t.NumMethod(); i++ {
       m := t.Method(i)
       fmt.Printf("%+v\n", m)
    }
}

程序输出

{Name:GetAge PkgPath: Type:func(*main.Student) int Func:<func(*main.Student) int Value> Index:0}
{Name:GetName PkgPath: Type:func(*main.Student) string Func:<func(*main.Student) string Value> Index:1}
{Name:GetScore PkgPath: Type:func(*main.Student) float64 Func:<func(*main.Student) float64 Value> Index:2}
{Name:SetAge PkgPath: Type:func(*main.Student, int) Func:<func(*main.Student, int) Value> Index:3}
{Name:SetName PkgPath: Type:func(*main.Student, string) Func:<func(*main.Student, string) Value> Index:4}
{Name:SetScore PkgPath: Type:func(*main.Student, float64) Func:<func(*main.Student, float64) Value> Index:5}
  • reflect.Type.NumMethod():返回struct所绑定的的方法个数
  • reflect.Type.Method(i):返回第i个方法的reflect.Method对象
    reflect.Method定义在src/reflect/type.go文件:
type Method struct {  
  Name    string // 方法名
  PkgPath string
  Type  Type  // 方法类型(
  Func  Value // 方法值(方法的接收器作为第一个参数)
  Index int   // 是结构体中的第几个方法
}

所以,通过reflect.Method对象,我们可以获取到struct所绑定的对应方法的方法名,方法类型等信息

3.3 通过反射调用方法

在上一小节我们知道了reflect.Type.Method(i)可以获取到struct所绑定的具体的方法对象reflect.Method,通过这个对象,我们不仅可以获取方法的详细信息,还可以动态的调用方法
其实在reflect.Value里我们也可以使用NumMethod()/Method(i)方法获取到对应的方法信息,不同的是reflect.Value.Method(i)返回的使一个reflect.Value对象,但是同样可以根据这个对象来动态调用方法,只是两者调用方法的方式有所区别
请看具体例子

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func (s *Student) GetName() string {
    return s.Name
}

func (s *Student) SetName(name string) {
    s.Name = name
}

func (s *Student) GetAge() int {
    return s.Age
}

func (s *Student) SetAge(age int) {
    s.Age = age
}

func (s *Student) GetScore() float64 {
    return s.Score
}

func (s *Student) SetScore(score float64) {
    s.Score = score
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    fmt.Printf("st === %+v\n", st)

    t := reflect.TypeOf(st)
    v := reflect.ValueOf(st)

    m1, ok := t.MethodByName("SetName")  // 获取SetName方法
    fmt.Printf("t get func by name:%t\n", ok)

    argsV1 := make([]reflect.Value, 0)
    argsV1 = append(argsV1, v)
    argsV1 = append(argsV1, reflect.ValueOf("lisi"))
    m1.Func.Call(argsV1)       // 
    fmt.Printf("st === %+v\n", st)

    m2 := v.MethodByName("SetName")    // 获取SetName方法
    argsV2 := make([]reflect.Value, 0)
    argsV2 = append(argsV2, reflect.ValueOf("wangwu"))
    m2.Call(argsV2)
    fmt.Printf("st === %+v\n", st)
}

程序输出

st === &{Name:zhangsan Age:18 Score:90.5}
t get func by name:true
st === &{Name:lisi Age:18 Score:90.5}
st === &{Name:wangwu Age:18 Score:90.5}

可以看到通过reflect.Type.MethodByName()方法获取到的reflect.Method对象和reflect.Value.MethodByName()方法获取到的reflect.Method获取到的reflect.Value对象都可以在程序运行时动态的调用方法修改结构本身,studentnamezhangsan------>lisi------>wangwu
但是二者的调用存在一个区别: 通过这个reflect.Method调用方法,必须使用Func字段,而且要传入接收器的reflect.Value作为第一个参数:

m1.Func.Call(argsV1) 

reflect.Value.MethodByName()返回一个reflect.Value对象,它不需要接收器的reflect.Value作为第一个参数,而且直接使用Call()发起方法调用

  m2.Call(argsV2)

3.4 通过反射设置值

在介绍通过反射设置或者说是修改值的方法之前,首先介绍一个概念,反射寻址。简单的说,可寻址就是可以根据地址找到值,在反射里面,reflect.Valuereflect.ValueOf()方法得到,根据命名就可以知道reflect.ValueOf()是得到一个值对象,显然他不能得到这个值的地址。所以通过reflect.ValueOf()方法得到的reflect.Value都是不可寻址的。在reflect包下有一个CanAddr()方法可以用于验证一个对象是否可寻址

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    v := reflect.ValueOf(st)
    fmt.Println(v.CanAddr())
}

程序输出

false

尽管这里st是一个Student类型的指针,但是经过reflect.ValueOf()之后得到的对象v,仍然是不能寻址的,他只能反映出当前指针指向的具体元素的地址,而当前指针自身所在的内存地址是无从得知的
经过前面学习指针反射类型的学习,我们知道了假设reflect.ValueOf()返回的是一个指针的reflect.Value对象,那么我们可以调用reflect.Value.Elem()方法得到具体的类型,而此时得到的这个反射具体类型就是可寻址的,我们可以知道他的地址,道理很简单,因为reflect.Value.Elem()获取到的值,记录了根据指针获取到值这个获取路径,显然我们可以根据值追溯到地址

  1. reflect.Value.Elem()方法得到具体的类型可寻址
    通过代码验证一下
package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    v := reflect.ValueOf(st)
    fmt.Println(v.Elem().CanAddr())
}

程序输出

true

2. 对切片进行反射时,通过reflect.Value.Index(i)获取到的reflect.Value对象是可以寻址的

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    slice := []int{12345}
    v := reflect.ValueOf(slice)
    fmt.Println(v.Index(0).CanAddr())
    fmt.Println(v.Index(1).CanAddr())
}

程序输出

true
true

但是对数组reflect.Value.Index(i)获取到的reflect.Value对象是不可寻址的

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    nums := [3]int{123}
    v1 := reflect.ValueOf(nums)
    fmt.Println(v1.Index(0).CanAddr())
}

程序输出

false

通过reflect.ValueOf得到的其实是原始数据的一份拷贝,切片底层实现其实是一个struct类型,struct里包含一个执行具体数组的指针,对切片拷贝,虽然拷贝了这个struct,自然struct里的指针也拷贝了一份,但是两个指针执行同一个内存区域,所以修改拷贝的切片,也会影响原值

  1. 通过结构体的指针获取到的字段也是可寻址
package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    v := reflect.ValueOf(st)
    f := v.Elem().Field(0)
    fmt.Println(f.CanAddr())
}

程序输出

true

通过上面分析,可寻址就是可以找到数据本身,而不是找到数据的副本,既然我们找到了数据本身,那么就可以对原数据修改,即设置值了吗?
大体上是这样,但是条件可能还要更严格一些,刚是可寻址还不够,比如当struct里含有未导出字段时,这个未导出字段是不可设置的。所以,在reflect包下有一个专门的方法用于判断这个值是否可修改:CanSet()
假设我们将Studentscore字段改为未导出的

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    score float64
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       score: 90.5,
    }
    v := reflect.ValueOf(st)
    f := v.Elem().Field(0)
    fmt.Println(f.CanSet())
    f2 := v.Elem().Field(2)
    fmt.Println(f2.CanSet())
}

程序输出

true
false

可以看到,可导出字段Name是可设置的,而不可导出字段score是不可设置的
当我们判定一个值是可设置之后,接下来要怎么修改这个值呢?
reflect.Value为基础类型提供了一系列特殊的Set方法:SetIntSetUintSetFloat等在反射的时候修改对应类型值
下面看个具体例子

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    v := reflect.ValueOf(st)
    f := v.Elem().Field(0)
    f.SetString("lisi")
    fmt.Printf("st = %+v\n", st)
}

程序输出

st = &{Name:lisi Age:18 Score:90.5}

可以看到,通过调用第一个字段reflect.valueSetString方法,将st对象的Name改为了lisi

3.5 结构体标签

我们在定义结构体的时候,可以为每个字段后面加一个标签,即StructTag,标签其实就是一组简直对,每个简直对用空格分开,这些标签信息可以通过反射获取

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string  `json:"name"`
    Age   int     `json:"age"`
    Score float64 `json:"score"`
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    t := reflect.TypeOf(st).Elem()
    for i := 0; i < t.NumField(); i++ {
       f := t.Field(i)
       fmt.Println(f.Tag)
    }
}

程序输出

json:"name"
json:"age"
json:"score"

4. 反射的优缺点

优点:

  • 可以提升程序代码的灵活性,根据条件在程序运行时灵活的调用函数,并且修改源代码结构

缺点:

  • 主要是性能影响,反射过程中会有大量的内存开辟和gc过程,导致程序的性能降低

资料分享

为了便于大家系统性的学习Go语言,秀才整理了Go语言学习入门,实战到精通的必看经典书籍 Go必看书籍封面

关注秀才的公众号:IT杨秀才,回复:Go,即可轻松领取

交流学习

如果您觉得文章有帮助,点个关注哦。可以关注公众号:IT杨秀才,秀才后面会在公众号分享Go语言:基础 》进阶 》探秘 》实战 》面试的系列知识。也会持续更新更多硬核文章,一起聊聊互联网那些事儿!