[翻译] effective go 之 Data

pengfei_xue · · 2899 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

Data

Allocation with new

Go has two allocation primitives, the built-in functions new and make. They do different things and apply to different types, which can be confusing, but the rules are simple. Let's talk about new first. It's a built-in function that allocates memory, but unlike its namesakes in some other languages it does not initialize the memory, it only zeros it. That is, new(T) allocates zeroed storage for a new item of type T and returns its address, a value of type *T. In Go terminology, it returns a pointer to a newly allocated zero value of type T.

Go有两种分配内存的方式 new 和 make 这两个函数所做的事情不一样 并且对应了不同的类型 听起来有点绕 但是它们的使用规则很简单 先讲new吧 new为变量分配内存 和其它语言中的new关键字不一样的是 它不会去对分配的内存做初始化 它仅仅用相应类型的零值赋给它 就是说 new(T)分配了一段已经赋为零值的内存给新创建的T类型变量 并且返回它的地址指向类型T的零值 (这段推荐看原文)

Since the memory returned by new is zeroed, it's helpful to arrange when designing your data structures that the zero value of each type can be used without further initialization. This means a user of the data structure can create one with new and get right to work. For example, the documentation for bytes.Buffer states that "the zero value for Buffer is an empty buffer ready to use." Similarly, sync.Mutex does not have an explicit constructor or Init method. Instead, the zero value for a sync.Mutex is defined to be an unlocked mutex.

由于分配的内存已经赋值为相应类型的零值 在设计数据结构的时候合理地利用这个特性 会带来额外的好处 你可以不用对它进一步初始化 就可以使用了 这意味着直接使用new创建的自定义数据类型 例如 bytes.Buffer的零值 就是一个已经可以使用的空buffer 类似地 sync.Mutex没有显式的构造函数 也没有Init初始化函数 而sync.Mutex的零值是没有锁上的互斥锁

The zero-value-is-useful property works transitively. Consider this type declaration.

零值即可使用的特性对我们来说是透明的 看下面这个例子

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

Values of type SyncedBuffer are also ready to use immediately upon allocation or just declaration. In the next snippet, both p and v will work correctly without further arrangement.

SyncedBuffer在用new或者直接由var创建后就是使用了 以下的这段代码中 p和v就是已经可以使用了 不需要再额外地进行初始化

p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  SyncedBuffer


Constructors and composite literals

Sometimes the zero value isn't good enough and an initializing constructor is necessary, as in this example derived from package os.

有的时候用new得到的零值满足不了我们的需求 我们希望有初始化构造函数 就如同下面这个os包中NewFile函数:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

There's a lot of boiler plate in there. We can simplify it using a composite literal, which is an expression that creates a new instance each time it is evaluated.

可以找到很多这样的例子 不过我们可以简化这样的初始化语句 省的一句一句地写 我们可以使用复合字面值 创建某个类型的实例:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}


Note that, unlike in C, it's perfectly OK to return the address of a local variable; the storage associated with the variable survives after the function returns. In fact, taking the address of a composite literal allocates a fresh instance each time it is evaluated, so we can combine these last two lines.

注意 这里有一个和C不一样的地方 我们可以返回一个局部变量的地址给函数的调用者 函数返回后 局部变量并没有被销毁 事实上 每次取复合字面值的地址(下面这个例子)都会重新创建一个新的File类型变量 所以上面那段代码的最后两句也可以这样写:

    return &File{fd, name, nil, 0}


The fields of a composite literal are laid out in order and must all be present. However, by labeling the elements explicitly as field:value pairs, the initializers can appear in any order, with the missing ones left as their respective zero values. Thus we could say

在使用复合字面值时 字面值的顺序必须与该类型的定义一致 而且定义中的每个字段都必须要有相应的字面值来初始化 但是 一旦明确地对标记出filed:value对时 每个字段出现的顺序可以与定义中的顺序不同 另外 使用这种方式的话 那些没有明确写出来的字段就是用该字段类型的零值来初始化 如下所示:

    return &File{fd: fd, name: name}


As a limiting case, if a composite literal contains no fields at all, it creates a zero value for the type. The expressions new(File) and &File{} are equivalent.

如果复合字面值没有包含任何字段 它会创建这个类型的零值 这个时候new(File) 和 &File{}是一样的

Composite literals can also be created for arrays, slices, and maps, with the field labels being indices or map keys as appropriate. In these examples, the initializations work regardless of the values of Enone, Eio, and Einval, as long as they are distinct.

复合字面值也可以用来创建数组 slice 或者map:

a := [...]string   {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} // 原文有误
s := []string      {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} // 原文有误
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}


Allocation with make

Back to allocation. The built-in function make(T, args) serves a purpose different from new(T). It creates slices, maps, and channels only, and it returns an initialized (notzeroed) value of type T (not *T). The reason for the distinction is that these three types are, under the covers, references to data structures that must be initialized before use. A slice, for example, is a three-item descriptor containing a pointer to the data (inside an array), the length, and the capacity, and until those items are initialized, the slice is nil. For slices, maps, and channels, make initializes the internal data structure and prepares the value for use. For instance,

make和new的用途不一样 make只能用来创建slice map和channel 它返回已经初始化过的T类型变量 需要单独使用make来创建这三种类型的变量有特殊的原因 这三种类型所引用的数据结构必须在初始化后才能使用 拿slice举例来说吧 它是包括三个元素 分别是 指向底层数据结构(array)的指针 长度 以及容量 在对这些元素进行初始化之前slice的值为nil make为slice map和channel初始化其内部的数据结构 看下面这个例子:

make([]int, 10, 100)

allocates an array of 100 ints and then creates a slice structure with length 10 and a capacity of 100 pointing at the first 10 elements of the array. (When making a slice, the capacity can be omitted; see the section on slices for more information.) In contrast, new([]int) returns a pointer to a newly allocated, zeroed slice structure, that is, a pointer to a nil slice value.

上面这个例子分配了一个长度为100的int数组 然后创建slice slice的长度为10 容量为100 这个新创建的slice指向底层数组的前10个元素 相较之下 new([]int)创建了一个新分配的slice 其值为nil


These examples illustrate the difference between new and make.

下面几个例子展示了new和make的差异:

var p *[]int = new([]int)       // allocates slice structure; *p == nil; rarely useful
var v  []int = make([]int, 100) // the slice v now refers to a new array of 100 ints

// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// Idiomatic:
v := make([]int, 100)

Remember that make applies only to maps, slices and channels and does not return a pointer. To obtain an explicit pointer allocate with new.

记住:make只能使用在map slice和channel上 要获取相应类型的指针 请使用new


Arrays 数组

Arrays are useful when planning the detailed layout of memory and sometimes can help avoid allocation, but primarily they are a building block for slices, the subject of the next section. To lay the foundation for that topic, here are a few words about arrays.

数组主要用来构建slice 下节我们会讲slice 为了给下节做个铺垫 这节就讲讲数组

There are major differences between the ways arrays work in Go and C. In Go,

Go与C中 数组的工作工作方式有很大区别:

  • Arrays are values. Assigning one array to another copies all the elements.
  • 数组就是值 可以直接把一个数组赋值给另一个数组 这会拷贝所有的元素
  • In particular, if you pass an array to a function, it will receive a copy of the array, not a pointer to it.
  • 如果你传递给函数一个数组 这个函数接受到的是这个数组的拷贝 而不是指向它的指针
  • The size of an array is part of its type. The types [10]int and [20]int are distinct.
  • 数组的大小是数组类型的一部分 [10]int 和 [20]int 是两个不同的类型

The value property can be useful but also expensive; if you want C-like behavior and efficiency, you can pass a pointer to the array.

数组是值这个特点很有用 但是代价也很高 如果你想像C一样高效 你可以给数组传递一个指针:

func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // Note the explicit address-of operator

But even this style isn't idiomatic Go. Slices are. 但这个并不是Go的习惯用法


Slices

Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for items with explicit dimension such as transformation matrices, most array programming in Go is done with slices rather than simple arrays.

slice对数组做了封装 在使用数组这样的数据时更加给力 更加易用 除非需要处理类似矩阵变换这样的问题 大多数数组操作还是由slice来完成的 

Slices are reference types, which means that if you assign one slice to another, both refer to the same underlying array. For instance, if a function takes a slice argument, changes it makes to the elements of the slice will be visible to the caller, analogous to passing a pointer to the underlying array. A Read function can therefore accept a slice argument rather than a pointer and a count; the length within the slice sets an upper limit of how much data to read. Here is the signature of the Read method of theFile type in package os:

slice是引用类型 也就是说 如果你把一个slice赋值给另一个slice 那么它们底层所指向的数组是同一个 举个例子 函数接受一个slice类型的参数 函数内对这个参数的操作 会直接影响到调用者 类似于C中 用指针作为参数 Read函数 就可以接受slice参数 而不是通过指针加元素个数作为参数 slice的长度已经为可以读多少数据设定了上限 下面这个是os包中File类型的Read方法的声明:

func (file *File) Read(buf []byte) (n int, err error)

The method returns the number of bytes read and an error value, if any. To read into the first 32 bytes of a larger buffer b, slice (here used as a verb) the buffer.

它返回成功读取的字节数 如果读操作中出现了错误 那么还会返回相应的错误值 读去b中的头32字节 可以参考下面这个例子:

    n, err := f.Read(buf[0:32])


Such slicing is common and efficient. In fact, leaving efficiency aside for the moment, the following snippet would also read the first 32 bytes of the buffer.

这样的例子很常见 也很高效 事实上 抛开效率不说 下面这段代码也可以读前32字节数据:

    var n int
    var err error
    for i := 0; i < 32; i++ {
        nbytes, e := f.Read(buf[i:i+1])  // Read one byte.
        if nbytes == 0 || e != nil {
            err = e
            break
        }
        n += nbytes
    }


The length of a slice may be changed as long as it still fits within the limits of the underlying array; just assign it to a slice of itself. The capacity of a slice, accessible by the built-in function cap, reports the maximum length the slice may assume. Here is a function to append data to a slice. If the data exceeds the capacity, the slice is reallocated. The resulting slice is returned. The function uses the fact that len and cap are legal when applied to the nil slice, and return 0.

slice的长度是可以伸缩的 只要它的长度没有超过底层数组的容量 slice的容量 可以通过函数cap来获得 这个函数返回slice的最大长度 下面这个函数给slice添加数据 如果超出了slice的容量 重新分配slice 并返回最终的slice 这个函数利用了len和cap函数 接收nil slice时 返回0

func Append(slice, data[]byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    for i, c := range data {
        slice[l+i] = c
    }
    return slice
}

We must return the slice afterwards because, although Append can modify the elements of slice, the slice itself (the run-time data structure holding the pointer, length, and capacity) is passed by value.

我们必须返回新建的slice 这是因为 虽然Append函数可以直接修改slice 但是slice本身是按值传递的

The idea of appending to a slice is so useful it's captured by the append built-in function. To understand that function's design, though, we need a little more information, so we'll return to it later.

给slice添加数据是非常频繁 有用的 Go中 可以直接使用append内置函数来达到这个目的 


Maps 

Maps are a convenient and powerful built-in data structure to associate values of different types. The key can be of any type for which the equality operator is defined, such as integers, floating point and complex numbers, strings, pointers, interfaces (as long as the dynamic type supports equality), structs and arrays. Slices cannot be used as map keys, because equality is not defined on them. Like slices, maps are a reference type. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller.

map又是一个非常有用的数据类型 可以把不同类型的数据联系到一起 map中的key可以是任何支持等于比较操作符的类型 例如int整数 浮点数 复数 字符串 指针 接口 结构体还有数组 slice不能用作map的key 因为slice并没有定义等于比较操作符 和slice一样 map也是引用类型 

Maps can be constructed using the usual composite literal syntax with colon-separated key-value pairs, so it's easy to build them during initialization.

map可以使用复合字面值 不同的key-value对的方式来初始化 

var timeZone = map[string] int {
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}


Assigning and fetching map values looks syntactically just like doing the same for arrays and slices except that the index doesn't need to be an integer.

赋值和取map的值 语法上看起来和数组 slice类似 但是index并不需要是整数

offset := timeZone["EST"]


An attempt to fetch a map value with a key that is not present in the map will return the zero value for the type of the entries in the map. For instance, if the map contains integers, looking up a non-existent key will return 0. A set can be implemented as a map with value type bool. Set the map entry to true to put the value in the set, and then test it by simple indexing.

用在map中不存在的key来取值时 会返回相应类型的零值 举例来说 如果map包含整数类型 用不存在的key来取值时 会返回0 集合可以使用bool类型的map来实现(值为bool类型) 如果某个key的值为true 那么这个值在这个集合中 反之亦反

attended := map[string] bool {
    "Ann": true,
    "Joe": true,
    ...
}

if attended[person] { // will be false if person is not in the map
    fmt.Println(person, "was at the meeting")
}

Sometimes you need to distinguish a missing entry from a zero value. Is there an entry for "UTC" or is that zero value because it's not in the map at all? You can discriminate with a form of multiple assignment.

有的时候你可能需要明确地知道某个key 是否存在 而map的key不存在即返回相应类型的零值这个特性 并不能满足我们的需求 map中不存在UTC这个key 还是UTC这个key对应的值就是0? 我们可以使用下面的形式来区别这两种情况:

var seconds int
var ok bool
seconds, ok = timeZone[tz]

For obvious reasons this is called the “comma ok” idiom. In this example, if tz is present, seconds will be set appropriately and ok will be true; if not, seconds will be set to zero and ok will be false. Here's a function that puts it together with a nice error report:

很明显啦 这个就是“comma ok” 这个例子中 如果tz存在 seconds就是正确的值 而ok是true 如果tz不存在 seconds的值为0 ok就是false offset函数把这两个值结合在一起 给出一个比较友好的错误报告:

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}


To test for presence in the map without worrying about the actual value, you can use the blank identifier (_). The blank identifier can be assigned or declared with any value of any type, with the value discarded harmlessly. For testing just presence in a map, use the blank identifier in place of the usual variable for the value.

如果只是想知道是否存在某个key 可以使用_ (虚标识符?) 看下面这个例子:

_, present := timeZone[tz]


To delete a map entry, use the delete built-in function, whose arguments are the map and the key to be deleted. It's safe to do this this even if the key is already absent from the map.

可以使用内置函数delete 删除map中的某个key-value对 这个函数的参数是map 以及要被删除的key 如果要删除的key根本不存在 删除操作也是ok的

delete(timeZone, "PDT")  // Now on Standard Time


Printing

Formatted printing in Go uses a style similar to C's printf family but is richer and more general. The functions live in the fmt package and have capitalized names:fmt.Printf, fmt.Fprintf, fmt.Sprintf and so on. The string functions (Sprintf etc.) return a string rather than filling in a provided buffer.

Go中的格式化输出和C的printf类似 但是功能更多 相关的格式化输出函数在fmt包中 包括:fmt.Printf, fmt.Fprinf fmt.Sprintf等函数 string相关的函数 Sprintf等 返回一个字符串 而不是填充已有的buffer(参见c)

You don't need to provide a format string. For each of Printf, Fprintf and Sprintf there is another pair of functions, for instance Print and Println. These functions do not take a format string but instead generate a default format for each argument. The Println versions also insert a blank between arguments and append a newline to the output while the Print versions add blanks only if the operand on neither side is a string. In this example each line produces the same output.

对于每一个Printf, Fprintf和Sprintf 都有一个对应的函数 比如Print和Println 这两个函数并不需要传入格式化字符串 但是它们会为每个参数提供默认的格式 Println函数同时会在各个参数间插入一个空白字符 并且在尾部添加一个换行符 而Print只有在相邻的两个参数都不是字符串时才添加空白字符 而且不会添加换行符:

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))


As mentioned in the Tour, fmt.Fprint and friends take as a first argument any object that implements the io.Writer interface; the variables os.Stdout and os.Stderr are familiar instances.

fmt.Fprint以及相关的函数 接受实现io.Writer接口的对象作为其第一个参数 os.Stdout os.Stderr是常见的对象

Here things start to diverge from C. First, the numeric formats such as %d do not take flags for signedness or size; instead, the printing routines use the type of the argument to decide these properties.

和C不一样的地方在于:首先 数值格式 比如%d并不像C那样 接受正负或者长度标记 输出函数使用它参数的类型来决定这些属性

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

prints 输出为:

18446744073709551615 ffffffffffffffff; -1 -1


If you just want the default conversion, such as decimal for integers, you can use the catch all format %v (for “value”); the result is exactly what Print and Println would produce. Moreover, that format can print any value, even arrays, slices, structs, and maps. Here is a print statement for the time zone map defined in the previous section.

如果只需要默认的格式转换 比如十进制整数 可以使用%v  输出的结果和Print, Println一样 而且这个格式%v可以以输出任何值 甚至是数值 slice 结构体 和map 下面这段代码输出上面例子中定义的时区map

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

which gives output 上述代码的输出为:

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]


For maps the keys may be output in any order, of course. When printing a struct, the modified format %+v annotates the fields of the structure with their names, and for any value the alternate format %#v prints the value in full Go syntax.

当然map的key输出 可能是乱序的 当输出结构体是 %v修饰符 %+v会输出结构体中的字段名 %#v输出该变量的Go语法格式 

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)

prints

&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}

(Note the ampersands注意&符号.) That quoted string format is also available through %q when applied to a value of type string or []byte; the alternate format %#q will use backquotes instead if possible. Also, %x works on strings, byte arrays and byte slices as well as on integers, generating a long hexadecimal string, and with a space in the format (% x) it puts spaces between the bytes.

%q在输出字符串 或者 []byte时 会加双引号 %#q则尽可能使用反引号 另外 %x也可以用在字符串 byte数组 byte slice 还有整数上 输出十六进制字符串 如果在%和x之间有一个空格 则在输出时会在不同的元素间添加空格

Another handy format is %T, which prints the type of a value.

还有一个很实用的形式是%T 它可以输出一个值的类型

fmt.Printf("%T\n", timeZone)

prints 输出

map[string] int


If you want to control the default format for a custom type, all that's required is to define a method with the signature String() string on the type. For our simple type T, that might look like this.

如果你像格式化 自定义的类型 只需要在相应类型上定义一个String() string 方法 比如:

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

to print in the format

输出为:

7/-2.35/"abc\tdef"

Our String method is able to call Sprintf because the print routines are fully reentrant and can be used recursively. We can even go one step further and pass a print routine's arguments directly to another such routine. The signature of Printf uses the type ...interface{} for its final argument to specify that an arbitrary number of parameters (of arbitrary type) can appear after the format.

我们定义的String可以调用Sprintf 因为输出相关的函数是可重入的 并且可以递归地调用 Printf函数的最后一个参数类型为...interface{} 也就是说它在格式化字符串后 可以接受任何数量任何类型的参数

func Printf(format string, v ...interface{}) (n int, err error) {

Within the function Printf, v acts like a variable of type []interface{} but if it is passed to another variadic function, it acts like a regular list of arguments. Here is the implementation of the function log.Println we used above. It passes its arguments directly to fmt.Sprintln for the actual formatting.

Printf函数中 v是[]interface{}类型的变量 但是当把它传递给另一个接受可变参数的函数时 它表现的和正常的参数列表一样 如下面这个例子:

// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

We write ... after v in the nested call to Sprintln to tell the compiler to treat v as a list of arguments; otherwise it would just pass v as a single slice argument.

v后面的...告诉编译器 把v当成是参数列表 否则编译器会把v当成是单个slice参数(单个参数 而不是多个参数组成的列表)


There's even more to printing than we've covered here. See the godoc documentation for package fmt for the details.

关于输出的更多内容 请参考godoc中fmt包的文档

By the way, a ... parameter can be of a specific type, for instance ...int for a min function that chooses the least of a list of integers:

...参数可以用于特定类型的参数 比如 min函数的...int参数 传递了多个整数给min函数

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}


Append

Now we have the missing piece we needed to explain the design of the append built-in function. The signature of append is different from our custom Append function above. Schematically, it's like this:

现在 我们来介绍一下内建函数append 下面是append函数的声明:

func append(slice []T, elements...T) []T 

where T is a placeholder for any given type. You can't actually write a function in Go where the type T is determined by the caller. That's why append is built in: it needs support from the compiler.

这里的T可以是任意的类型 在Go中 你不可能写个函数 它的参数类型由调用者来决定 这也就是为什么append是内置函数 它需要编译器的帮助

What append does is append the elements to the end of the slice and return the result. The result needs to be returned because, as with our hand-written Append, the underlying array may change. This simple example

append会在slice的末尾添加一个元素 并且返回结果 最终的slice需要被返回 是因为slice对应的底层数组可能已经改变了 看几个简单的例子:

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

prints [1 2 3 4 5 6]. So append works a little like Printf, collecting an arbitrary number of arguments.

append和Printf类似 都可以接受任意数量的参数


But what if we wanted to do what our Append does and append a slice to a slice? Easy: use ... at the call site, just as we did in the call to Output above. This snippet produces identical output to the one above.

如果我们想给slice添加一个slice该怎么做呢 很简单 可以使用... 如下所示:

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

Without that ..., it wouldn't compile because the types would be wrong; y is not of type int.

没有...的话 编译通不过 因为y的类型不是整数int


有疑问加站长微信联系(非本文作者)

本文来自:开源中国博客

感谢作者:pengfei_xue

查看原文:[翻译] effective go 之 Data

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2899 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传