返回顶部
首页 > 资讯 > 后端开发 > GO >Golang中反射的常见用法分享
  • 749
分享到

Golang中反射的常见用法分享

Golang反射常见用法Golang反射用法Golang反射 2023-01-04 15:01:28 749人浏览 独家记忆
摘要

目录根据类型做不同处理标准库 JSON 中的示例基本类型的反射数组类型的反射chan 反射map 反射迭代反射 map 对象slice 反射string 反射interface/Po

在之前的两篇文章 《深入理解 Go reflect - 反射基本原理》、《深入理解 go reflect - 要不要传指针》 中, 我们讲解了关于 go 反射的一些基本原理,以及通过反射对象修改变量的一些注意事项。 本篇文章将介绍一些常见的反射用法,涵盖了常见的数据类型的反射操作。

根据类型做不同处理

使用反射很常见的一个场景就是根据类型做不同处理,比如下面这个方法,根据不同的 Kind 返回不同的字符串表示:

func getType(i interface{}) string {
   v := reflect.ValueOf(i)

   switch v.Kind() {
   case reflect.Bool:
      b := "false"
      if v.Bool() {
         b = "true"
      }
      return fmt.Sprintf("bool: %s", b)
   case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
      return fmt.Sprintf("int: %d", v.Int())
   case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
      return fmt.Sprintf("uint: %d", v.Uint())
   case reflect.Float32, reflect.Float64:
      return fmt.Sprintf("float: %.1f", v.Float())
   case reflect.String:
      return fmt.Sprintf("string: %s", v.String())
   case reflect.Interface:
      return fmt.Sprintf("interface: %v", v.Interface())
   case reflect.Struct:
      return fmt.Sprintf("struct: %v", v.Interface())
   case reflect.Map:
      return fmt.Sprintf("map: %v", v.Interface())
   case reflect.Slice:
      return fmt.Sprintf("slice: %v", v.Interface())
   case reflect.Array:
      return fmt.Sprintf("array: %v", v.Interface())
   case reflect.Pointer:
      return fmt.Sprintf("pointer: %v", v.Interface())
   case reflect.Chan:
      return fmt.Sprintf("chan: %v", v.Interface())
   default:
      return "unknown"
   }
}

func TestKind(t *testing.T) {
   assert.Equal(t, "int: 1", getType(1))
   assert.Equal(t, "string: 1", getType("1"))
   assert.Equal(t, "bool: true", getType(true))
   assert.Equal(t, "float: 1.0", getType(1.0))

    arr := [3]int{1, 2, 3}
    sli := []int{1, 2, 3}
    assert.Equal(t, "array: [1 2 3]", getType(arr))
    assert.Equal(t, "slice: [1 2 3]", getType(sli))
}

标准库 json 中的示例

在标准库 encoding/json 中,也有类似的场景,比如下面这个方法,根据不同的 Kind 做不同的处理:

func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
   // ... 其他代码
   switch t.Kind() {
   case reflect.Bool:
      return boolEncoder
   case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
      return intEncoder
   case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
      return uintEncoder
   // ...省略其他 case...
   default:
      return unsupportedTypeEncoder
   }
}

在进行 json 编码的时候,因为不知道传入的参数是什么类型,所以需要根据类型做不同的处理,这里就是使用反射来做的。 通过判断不同的类型,然后返回不同的 encoder

基本类型的反射

这里说的基本类型是:int*uint*float*complex*bool 这种类型。

通过反射修改基本类型的值,需要注意的是,传入的参数必须是指针类型,否则会 panic

func TestBaseKind(t *testing.T) {
   // 通过反射修改 int 类型变量的值
   a := 1
   v := reflect.ValueOf(&a)
   v.Elem().SetInt(10)
   assert.Equal(t, 10, a)

   // 通过反射修改 uint16 类型变量的值
   b := uint16(10)
   v1 := reflect.ValueOf(&b)
   v1.Elem().SetUint(20)
   assert.Equal(t, uint16(20), b)

   // 通过反射修改 float32 类型变量的值
   f := float32(10.0)
   v2 := reflect.ValueOf(&f)
   v2.Elem().SetFloat(20.0)
   assert.Equal(t, float32(20.0), f)
}

通过反射修改值的时候,需要通过 Elem() 方法的返回值来修改。

数组类型的反射

通过反射修改数组中元素的值,可以使用 Index 方法取得对应下标的元素,然后再使用 Set 方法修改值:

func TestArray(t *testing.T) {
   // 通过反射修改数组元素的值
   arr := [3]int{1, 2, 3}
   v := reflect.ValueOf(&arr)
   // 修改数组中的第一个元素
   v.Elem().Index(0).SetInt(10)
   assert.Equal(t, [3]int{10, 2, 3}, arr)
}

chan 反射

我们可以通过反射对象来向 chan 中发送数据,也可以从 chan 中接收数据:

func TestChan(t *testing.T) {
   // 通过反射修改 chan
   ch := make(chan int, 1)
   v := reflect.ValueOf(&ch)
   // 通过反射对象向 chan 发送数据
   v.Elem().Send(reflect.ValueOf(2))
   // 在反射对象外部从 chan 接收数据
   assert.Equal(t, 2, <-ch)
}

map 反射

通过反射修改 map 中的值,可以使用 SetMapindex 方法修改 map 中对应的 key

func TestMap(t *testing.T) {
   // 通过反射修改 map 元素的值
   m := map[string]int{"a": 1}
   v := reflect.ValueOf(&m)
   // 修改 a 的 key,修改其值为 2
   v.Elem().SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(2))
   // 外部的 m 可以看到反射对象的修改
   assert.Equal(t, 2, m["a"])
}

迭代反射 map 对象

我们可以通过反射对象的 MapRange 方法来迭代 map 对象:

func TestIterateMap(t *testing.T) {
   // 遍历 map
   m := map[string]int{"a": 1, "b": 2}
   v := reflect.ValueOf(m)
   // 创建 map 迭代器
   iter := v.MapRange()

   // 迭代 map 的元素
   for iter.Next() {
      // a 1
      // b 2
      fmt.Println(iter.Key(), iter.Value())
   }
}

slice 反射

通过反射修改 slice 中的值,可以使用 Index 方法取得对应下标的元素,然后再使用 Set* 方法修改值,跟数组类似:

func TestSlice(t *testing.T) {
   // 通过反射修改 slice 元素的值
   sli := []int{1, 2, 3}
   v := reflect.ValueOf(&sli)
   v.Elem().Index(0).SetInt(10)
   assert.Equal(t, []int{10, 2, 3}, sli)
}

string 反射

对于 string 类型,我们可以通过其反射对象的 String 方法来修改其内容:

func TestString(t *testing.T) {
   // 通过反射修改字符串的值
   s := "hello"
   v := reflect.ValueOf(&s)
   v.Elem().SetString("world")
   assert.Equal(t, "world", s)
}

interface/Pointer 反射

对于 interfacePointer 类型,我们可以通过其反射对象的 Elem 方法来修改其内容:

func TestPointer(t *testing.T) {
   a := 1

   // 接口类型
   var i interface{} = &a
   v1 := reflect.ValueOf(i)
   v1.Elem().SetInt(10)
   assert.Equal(t, 10, a)

   // 指针类型
   var p = &a
   v2 := reflect.ValueOf(p)
   v2.Elem().SetInt(20)
   assert.Equal(t, 20, a)
}

这两种类型,我们都需要通过 Elem 方法来先获取其实际保存的值,然后再修改其值。

结构体的反射

对于 go 中的结构体,反射系统中为我们提供了很多操作结构体的方法,比如获取结构体的字段、方法、标签、通过反射对象调用其方法等。

先假设我们有如下结构体:

type Person struct {
   Name string
   Age  int

   sex uint8
}

func (p Person) M1() string {
   return "person m1"
}

func (p *Person) M2() string {
   return "person m2"
}

遍历结构体字段

我们可以通过 NumField 方法来获取结构体的字段数量,然后通过 Field 方法来获取结构体的字段:

func TestStruct1(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)

   // string Tom
   // int 18
   // uint8 1
   for i := 0; i < v.NumField(); i++ {
      fmt.Println(v.Field(i).Type(), v.Field(i))
   }
}

根据名称或索引获取结构体字段

我们可以根据结构体字段的名称或索引来获取结构体的字段:

func TestStruct2(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)

   assert.Equal(t, 18, v.Field(1).Interface())
   assert.Equal(t, 18, v.FieldByName("Age").Interface())
   assert.Equal(t, 18, v.FieldByIndex([]int{1}).Interface())
}

修改结构体字段

我们可以通过 Field 方法来获取结构体的字段,然后再使用 Set* 方法来修改其值:

func TestStruct2(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(&p)

   v.Elem().FieldByName("Name").SetString("Jack")

   assert.Equal(t, "Jack", p.Name)
}

上面因为 Namestring 类型,所以我们使用 SetString 方法来修改其值,如果是 int 类型,我们可以使用 SetInt 方法来修改其值,依此类推。

结构体方法调用

通过反射对象来调用结构体的方法时,需要注意的是,如果我们需要调用指针接收者的方法,则需要传递地址

func TestStruct3(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}

   // 值接收者(receiver)
   v1 := reflect.ValueOf(p)
   assert.Equal(t, 1, v1.NumMethod())
   // 注意:值接收者没有 M2 方法
   assert.False(t, v1.MethodByName("M2").IsValid())

   // 通过值接收者调用 M1 方法
   results := v1.MethodByName("M1").Call(nil)
   assert.Len(t, results, 1)
   assert.Equal(t, "person m1", results[0].Interface())

   // 指针接收者(pointer receiver)
   v2 := reflect.ValueOf(&p)
   assert.Equal(t, 2, v2.NumMethod())

   // 通过指针接收者调用 M1 和 M2 方法
   results = v2.MethodByName("M1").Call(nil)
   assert.Len(t, results, 1)
   assert.Equal(t, "person m1", results[0].Interface())

   results = v2.MethodByName("M2").Call(nil)
   assert.Len(t, results, 1)
   assert.Equal(t, "person m2", results[0].Interface())
}

说明:

  • 结构体参数是值的时候,reflect.ValueOf 返回的反射对象只能调用值接收者的方法,不能调用指针接收者的方法。
  • 结构体参数是指针的时候,reflect.ValueOf 返回的反射对象可以调用值接收者和指针接收者的方法。
  • 调用 MethodByName 方法时,如果方法不存在,则返回的反射对象的 IsValid 方法返回 false
  • 调用 Call 方法时,如果没有参数,传 nil 参数即可。如果方法没有返回值,则返回的结果切片为空。
  • 调用 Call 方法的参数是 reflect.Value 类型的切片,返回值也是 reflect.Value 类型的切片。

是否实现接口

对于这个,其实有一个更简单的方法,那就是利用接口断言:

func TestStrunct4_0(t *testing.T) {
   type TestInterface interface {
      M1() string
   }

   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)

   // v.Interface() 返回的是 interface{} 类型
   // v.Interface().(TestInterface) 将 interface{} 类型转换为 TestInterface 类型
   v1, ok := v.Interface().(TestInterface)
   assert.True(t, ok)
   assert.Equal(t, "person m1", v1.M1())
}

另外一个方法是,通过反射对象的 Type 方法获取类型对象,然后调用 Implements 方法来判断是否实现了某个接口:

func TestStruct4(t *testing.T) {
   type TestInterface interface {
      M1() string
   }

   var p = Person{Name: "Tom", Age: 18, sex: 1}
   typ := reflect.TypeOf(p)

   typ1 := reflect.TypeOf((*TestInterface)(nil)).Elem()
   assert.True(t, typ.Implements(typ1))
}

结构体的 tag

这在序列化、反序列化、ORM 库中用得非常多,常见的 validator 库也是通过 tag 来实现的。 下面的例子中,通过获取变量的 Type 就可以获取其 tag 了:

type Person1 struct {
   Name string `json:"name"`
}

func TestStruct5(t *testing.T) {
   var p = Person1{Name: "Tom"}
   typ := reflect.TypeOf(p)
   tag := typ.Field(0).Tag

   assert.Equal(t, "name", tag.Get("json"))
}

修改结构体未导字段

我们知道,结构体的字段如果首字母小写,则是未导出的,不能被外部包访问。但是我们可以通过反射修改它:

func TestStruct6(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(&p)

   // 下面这样写会报错:
   // panic: reflect: reflect.Value.SetInt using value obtained using unexported field
   // v.Elem().FieldByName("sex").SetInt(0)
   ft := v.Elem().FieldByName("sex")

   sexV := reflect.NewAt(ft.Type(), unsafe.Pointer(ft.UnsafeAddr())).Elem()

   assert.Equal(t, 1, p.sex)           // 修改前是 1
   sexV.Set(reflect.ValueOf(uint8(0))) // 将 sex 字段修改为 0
   assert.Equal(t, 0, p.sex)           // 修改后是 0
}

这里通过 NewAt 方法针对 sex 这个未导出的字段创建了一个指针,然后我们就可以通过这个指针来修改 sex 字段了。

方法的反射

这里说的方法包括函数和结构体的方法。

入参和返回值

reflect 包中提供了 InOut 方法来获取方法的入参和返回值:

func (p Person) Test(a int, b string) int {
   return a
}

func TestMethod(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)

   m := v.MethodByName("Test")
   // 参数个数为 2
   assert.Equal(t, 2, m.Type().NumIn())
   // 返回值个数为 1
   assert.Equal(t, 1, m.Type().NumOut())

   // In(0) 是第一个参数,In(1) 是第二个参数
   arg1 := m.Type().In(0)
   assert.Equal(t, "int", arg1.Name())
   arg2 := m.Type().In(1)
   assert.Equal(t, "string", arg2.Name())

   // Out(0) 是第一个返回值
   ret0 := m.Type().Out(0)
   assert.Equal(t, "int", ret0.Name())
}

说明:

  • InOut 方法返回的是 reflect.Type 类型,可以通过 Name 方法获取类型名称。
  • NumInNumOut 方法返回的是参数和返回值的个数。
  • reflect.Value 类型的 MethodByName 方法可以获取结构体的方法。

通过反射调用方法

reflect.Value 中对于方法类型的反射对象,有一个 Call 方法,可以通过它来调用方法:

func TestMethod2(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)

   // 通过反射调用 Test 方法
   m := v.MethodByName("Test")
   arg1 := reflect.ValueOf(1)
   arg2 := reflect.ValueOf("hello")
   args := []reflect.Value{arg1, arg2}

   rets := m.Call(args)
   assert.Len(t, rets, 1)
   assert.Equal(t, 1, rets[0].Interface())
}

说明:

  • Call 方法的参数是 []reflect.Value 类型,需要将参数转换为 reflect.Value 类型。
  • Call 方法的返回值也是 []reflect.Value 类型。
  • reflect.Value 类型的 MethodByName 方法可以获取结构体的方法的反射对象。
  • 通过方法的反射对象的 Call 方法可以实现调用方法。

总结

  • 通过 reflect.Kind 可以判断反射对象的类型,Kind 涵盖了 go 中所有的基本类型,所以反射的时候判断 Kind 就足够了。
  • 如果要获取反射对象的值,需要传递指针给 reflect.Value
  • 可以往 chan 的反射对象中发送数据,也可以从 chan 的反射对象中接收数据。
  • SetMapIndex 方法可以修改 map 中的元素。MapRange 方法可以获取 map 的迭代器。
  • 可以通过 Index 方法获取 slice 的元素,也可以通过 SetIndex 方法修改 slice 的元素。
  • 可以通过 SetString 方法修改 string 的值。
  • 对于 interfacePointer 类型的反射对象,可以通过 Elem 方法获取它们的值,同时也只有通过 Elem 获取到的反射对象能调用 Set* 方法来修改其指向的对象。
  • reflect 包中提供了很多操作结构体的功能:如获取结构体的字段、获取结构体的方法、调用结构体的方法等。我们使用一些类库的时候,会需要通过结构体的 tag 来设置一些元信息,这些信息只有通过反射才能获取。
  • 我们可以通过 NewAt 来创建一个指向结构体未导出字段的反射对象,这样就可以修改结构体的未导出字段了。
  • 对于函数和方法,go 的反射系统也提供了很多功能,如获取参数和返回值信息、使用 Call 来调用函数和方法等。

到此这篇关于golang中反射的常见用法分享的文章就介绍到这了,更多相关Golang反射内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文档:

--结束END--

本文标题: Golang中反射的常见用法分享

本文链接: https://lsjlt.com/news/176705.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Golang中反射的常见用法分享
    目录根据类型做不同处理标准库 json 中的示例基本类型的反射数组类型的反射chan 反射map 反射迭代反射 map 对象slice 反射string 反射interface/Po...
    99+
    2023-01-04
    Golang反射常见用法 Golang反射用法 Golang反射
  • golang反射常见用法有哪些
    golang反射常见用法有:1、获取对象的类型信息;2、获取和设置对象的属性值;3、动态调用对象的方法;4、是一种复杂的机制。本教程操作环境:windows10系统、golang1.20.1版本、DELL G3电脑。Golang是一种现代的...
    99+
    2023-07-14
  • 深入理解Golang反射机制及其常见用法
    反射机制在 go 中允许程序动态检查和操作类型信息和值。其基本类型 value 和 type 分别表示值的反射对象和类型信息,提供了一系列操作和检查方法。反射机制在实践中可用于动态类型检...
    99+
    2024-04-03
    golang 反射
  • golang反射机制的基本概念和常见用法有哪些
    这篇文章主要介绍了golang反射机制的基本概念和常见用法有哪些的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇golang反射机制的基本概念和常见用法有哪些文章都会有所收获,下面我们一起来看看吧。前言golan...
    99+
    2023-07-05
  • Golang中常用的语法糖分享
    目录1、名字由来2、Golang常用语法糖2.1 简短变量声明 :=2.2 可变参函数 ...2.3 new函数1、名字由来 语法糖(Syntactic sugar)的概念是由英国计...
    99+
    2023-05-20
    Golang常用语法糖 Golang语法糖 Go 语法糖
  • 一文带你了解Golang中reflect反射的常见错误
    目录获取 Value 的值之前没有判断类型没有传递指针给 reflect.ValueOf在一个无效的 Value 上操作什么时候 IsValid 返回 false其他情况下 IsVa...
    99+
    2023-01-05
    Golang reflect反射错误 Golang reflect反射 Golang reflect
  • 常见的反爬虫urllib技术分享
    目录通过robots.txt来限制爬虫:通过User-Agent来控制访问:验证码:IP限制:cookie:JS渲染:爬虫和反爬的对抗一直在进行着…为了帮助更好的进行爬...
    99+
    2024-04-02
  • 【总结分享】golang中常见的类型转换方法
    Golang是一种强类型的编程语言,因此在处理不同类型之间的数据时,需要进行类型转换。本文将介绍Golang中常见的类型转换方法。数字类型转换在Golang中,数字类型转换是最常见的类型转换。常见的数字类型包括int、float32和flo...
    99+
    2023-05-14
    类型转换 Golang go语言
  • golang 反射调用方法
    反射是 Golang 中十分重要的一部分,它允许我们在运行时动态地查看和修改对象的类型、属性和方法。这种机制使得我们有了更多的灵活性和强大的能力。本文将重点介绍使用 Golang 反射调用方法的方法和技巧,希望对读者有所帮助。一、反射调用方...
    99+
    2023-05-15
  • golang反射调用方法
    前言在 Golang 中,反射机制是一种重要的功能,它可以在运行时动态地获取变量的类型和值,并可以动态地调用方法,特别是对于一些具有通用性的代码实现和一些框架的实现,反射机制是必不可少的。本文将通过一个实例来介绍在 Golang 中如何使用...
    99+
    2023-05-15
  • Java中反射的学习笔记分享
    目录简介一个简单的例子设置使用反射模拟instanceof运算了解类的方法获取有关构造函数的信息查找类字段按名称调用方法创建新对象更改字段的值使用数组总结简介 反射是Java编程语言...
    99+
    2022-11-13
    Java 反射
  • 怎么分析Golang的反射reflect
    本篇文章为大家展示了怎么分析Golang的反射reflect,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。编程语言中反射的概念在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这...
    99+
    2023-06-29
  • java Stream流常见操作方法(反射,类加载器,类加载,反射)
    目录Stream流常见的中间操作方法Stream流中常见的终结操作方法反射类加载器反射概述Stream流常见的中间操作方法 Streamfilter(Predicate predic...
    99+
    2024-04-02
  • Golang断言的常见使用情景和技巧分享
    Golang断言的常见应用场景及技巧分享 在Go语言中,断言是一种类型转换的机制,用于在运行时判断一个接口类型对象是否实现了某个具体的接口或者是某种具体的数据类型。本文将分享一些Golang断言的常见应用场景...
    99+
    2024-01-29
    应用场景 Golang 断言 Golang断言
  • golang的反射有什么用
    在 go 语言中,反射机制允许程序在运行时检查和操作自身类型和值,用途广泛,包括:类型检查、转换和创建新的数据结构元编程和代码生成调试和测试泛型处理第三方库集成 Go 语言中的反射用途...
    99+
    2024-04-21
    python golang
  • Golang 中反射的应用实例详解
    目录引言Golang类型设计原则Golang 中为什么要使用反射/什么场景可以(应该)使用反射举例场景:反射的基本用法反射的性能分析与优缺点测试反射结构体初始化测试结构体字段读取/赋...
    99+
    2024-04-02
  • 一文详解Golang中的反射
    本篇文章带大家主要来聊聊Golang中反射,希望对你有新的认知。虽然很多人使用 Go 语言有一定时间了,甚至有的使用了 1 年 2 年,然后对于 Go 语言中的反射还是模棱两可,使用起来的时候,心里也不是非常有底气。【相关推荐:Go视频教程...
    99+
    2023-05-14
    反射 go语言 Golang
  • C#中反射方法的用法
    这篇文章主要讲解了“C#中反射方法的用法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C#中反射方法的用法”吧!首先我们建立一个类库,将它生成为HelloWorld.dll:usingSys...
    99+
    2023-06-18
  • 分享MySQL常用 内核 Debug 几种常见方法
    目录一、准备Debug环境二、使用GDB调试启动GDB编译器GDB常用命令Debug示例1、取变量值2、调试脚本三、使用Trace文件调试设置debug参数Debug示例阅读本文你将...
    99+
    2024-04-02
  • JavaScript中常见的BUG及其修复方法分享
    这篇文章主要讲解了“JavaScript中常见的BUG及其修复方法分享”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“JavaScript中常见的BUG及其修复方法分享”吧!如今网站几乎100...
    99+
    2023-06-04
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作