前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于go语言的几个陷阱

关于go语言的几个陷阱

作者头像
李海彬
发布2019-05-08 14:19:28
1K0
发布2019-05-08 14:19:28
举报
文章被收录于专栏:Golang语言社区

作者:googege 链接:https://www.jianshu.com/p/dcfcf9eeb10c 來源:简书

  1. 闭包

所谓闭包就是指一个函数中的函数,并且这个函数可以调用外部的变量并且无论使用多少次, 都可以一直拥有这个变量不回收,那么这个变量可以称为闭包变量。

  1. 循环体变量
代码语言:javascript
复制
 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7func main(){
 8    tt()
 9}
10func tt() {
11    for i := 0; i < 10; i++ {
12        go func() {
13            time.Sleep(1e3)
14            fmt.Println(i)
15        }()
16    }
17    time.Sleep(1e9)
18}

这个函数执行的时候 tt中打印出来的是10 原因也是很简单,因为go在初始化的时候先初始化参数量,全局先初始参数再看函数,在函数内部先初始参数再进行运算,所以 就造成在for执行完后 这里的i是同样的i 以为初始化的参数 i一直会变,但是都是这个变量本身,又因为for循环比内部的函数速度快很多,导致,当for循环进行完了,这些函数还没正式开始运行,然后i就取最终值 10了

  1. return 和 defer 的执行顺序

在go里

代码语言:javascript
复制
 1package main
 2
 3import "fmt"
 4
 5func tt() int {
 6    var i = 0
 7    defer func() {
 8        fmt.Println(i)
 9        i++
10        fmt.Println(i)
11    }()
12
13    return i
14}
15
16func main() {
17    tt()
18}

在这个函数中 执行顺序是这样的 首先先初始化 i = 0 然后 defer无法初始化(参数变量)因为它没有 然后到了return 然后 直接执行了后面的内容 没错 什么都没有就是i而已 然后 开始了return 直接将值返回了,这时候它没有结束 因为有defer 所以开始执行了defer,然后defer中i是几?嗯 是0 因为这个时候i是0了 然后 打印了0 i再次++ i等于1了,然而并没有什么机会去用这个i了,因为 已经return过了,所以这个i就被收回了,加入return后面是一个闭包,那么这个i 就有用了,它就不会被收回。

然后 这个时候函数就结束了。

看一下 这几种特殊情况

代码语言:javascript
复制
 1// return 1 "defer tt1 0"
 2func tt1() int {
 3    var i = 0
 4    defer fmt.Println("defer  tt1", i)
 5    i++
 6    return i
 7}
 8
 9// return 1 "defer tt1 0"
10func tt2() int {
11    var i = 0
12    defer func(i int) {// 参数复制,值的复制。
13        fmt.Println("defer tt2:", i)
14    }(i)
15    i++
16    return i
17}
18
19// 1 1
20func tt3()int{
21    var i =0
22    defer func() {
23        fmt.Println("defer tt3",i)
24        i++
25    }()
26    i++
27    return i
28}
29
30//1 2
31//2
32func tt4() func() int {
33    var i = 0
34    defer func() {
35        fmt.Println("defer tt4:",i)
36        i++
37    }()
38
39    i++
40    return func() int {
41
42        return i// 引用变量。
43    }
44}
45--------
46package main
47
48import "fmt"
49// 0 13
50func tt5() (num int) {
51    defer func() {
52        fmt.Println("dd", num)
53        num++ //这里的num的作用于属于上层的函数体
54    }()
55    return 12// 返回值是函数运行最后的num值,很明显是defer中的num值。然后返回1
56    // 这种形式的返回值,因为return 后面什么都没,所以它就会查找这个函数域内的最后值。因为函数的运行是
57    //运行return后面的的内容 + defer + 隐藏的os.Exit()
58    普通模式下是 运行return后面的内容 +返回return后面的内容 +运行defer + os.Exit()
59
60}
61func main(){
62    fmt.Println(tt5())
63}
64
65func main() {
66    fmt.Println(tt5())
67    fmt.Println(tt6())
68}
69//dd 0 dd1 2 return 1
70func tt6() (num int) {
71    defer func(num int) {//// 使用这种形式 defer的函数内部的值已经是num的一个拷贝了,所以它里面怎么改变都不影响外部的return
72        fmt.Println("dd", num)
73        num++// 这里的num属于本层函数的作用域,所以它无法改变外层函数的数值。
74        num++
75        fmt.Println("dd1", num)
76    }(num)
77    num++
78    return
79
80}
  1. 变量的调用和直接改变参数本身
代码语言:javascript
复制
 1func tt(){
 2var i
 3dd(i)
 4fmt.Println(i)
 5
 6}
 7
 8func dd(i int){
 9i++
10fmt.Println(i)
11}
12
13// 1 0

原因就是dd(i)这个是代表了 赋值,也就是将var的值赋予了dd的形参 可以看做是 d = i dd(d) 这个叫做赋值 然后值的拷贝或者是指针的传入以及指针的获取实际值是这个地方的问题

然后还有一种是这样的

代码语言:javascript
复制
1func tt(){
2var i = 0
3{
4i = 12
5}
6fmt.Println(i)
7}

指针来更更改i其实是错误的理解,因为这个地方的i = 12 压根就没有赋值 这一种说法它不过是更改自己的值而已,就像上面的那个函数即使使用指针, 那么更改指针的实际值的时候也是这么干的,所以i = 12 只是这个参数的在 调整自己的值罢了,它是改变的自己,这里就不牵涉到 是值的拷贝还是引用的拷贝了 因为它压根没有拷贝,仅仅是改变自己罢了。

  1. 值的方法和指针的方法

首先 指针的方法和值的方法可以互相调用,因为go会自动帮你 比如指针的方法g()你使用了值来调用那么go会帮你自动取地址相对的如果是 值的方法 go自动帮你取 *

第二个地方 首先 值上面可以有方法 指针上面也是有方法,我们谈的是 关于对象的方法这点先阐述 因为除了struct(对象)其它类型除了 指针和nil都可以有自己的方法 其它类型不讨论

就是指针的方法的时候,那么go会自动帮你取这个对象的,因为指针没办法取得对象 里面的value值,只能去得到,但是go帮你取了,所以你可以使用看似 指针直接去值。

  1. 关于实现接口

这个地方go很严格,首先就是接口类型的变量不允许取指针,本来它就是引用类型了(初始化是nil)nil取不到method。 虽然slice这种也是引用类型但是go允许你取它的指针,但是接口类型不允许取(取了也没有意义)

而且对于实现一个接口来说如果你是指针类型实现的接口,那么将变量传递给指针类型时

也必须是指针类型的变量,值同样,不允许自动取&或者* 举个例子

代码语言:javascript
复制
 1type a interface{
 2
 3get()
 4}
 5type b struct {
 6c value
 7}
 8
 9func(b1 *b)get(){
10fmt.Print(b1.c)// go帮你自动取了 *b1这个地方不变,go帮你自动取对象的值,在这个地方也可以。
11}
12func ddc(a1 a){
13a1.get()
14}
15
16func main(){
17b1 := new(b)
18var b2 b
19ddc(b1)// 正确
20ddc(b2)// 错误❌
21}
  1. 关于slice

slice赋值的时候可以不用指定类型

代码语言:javascript
复制
1type t int
2slice := make([]t,10)
3
4slice = [][]int{
5{1}, //不需要使用 t{1}
6{2},
7{3},
8}

如果slice里面还是slice或者是map等这种引用类型的话是这么处理的

代码语言:javascript
复制
1slice := make([][]int,10)
2slice[0] = make([]int,4)
3// 或者
4slice[1] = []int{
51,2,3,}

因为 引用类型不初始化的话 本身就是nil 所以会panic

  1. 关于变量的初始化

关于这个地方我也出错过

代码语言:javascript
复制
 1func dd(t *int){
 2fmt.Println(*t)
 3}
 4func main(){
 5var t *int
 6
 7dd(t)
 8
 9
10}

这样就会出错,因为 所有的变量都会初始化(go没有声明 go会自动初始化) 但是t是个指针类型,它的初始化就是nil,所以*nil是错误的,正确的方法是

代码语言:javascript
复制
 1func dd(t *int){
 2fmt.Println(*t)
 3}
 4func main(){
 5var t int
 6
 7dd(&t)
 8
 9
10}

或者优雅一点

代码语言:javascript
复制
 1func dd(t *int){
 2fmt.Println(*t)
 3}
 4func main(){
 5t := new(int)
 6
 7dd(t)
 8
 9
10}
  1. slice 关于他的len和cap

不要超过它的len来查找数据。(而不是cap)只要是超过了len就会报错,虽然没有超过cap 但是它的out of range 错误是根据len来定的。

  1. 不要获取map的值的地址
代码语言:javascript
复制
1t := make(map[string]string)
2t["12"] = 12
3fm.println(&t["12"])

因为map是动态的,所以它的value的值 的地址不是固定的所以go不允许取得 它的地址。

但是 slice可以。

代码语言:javascript
复制
1b := make([]int,12)
2    b[1] = 12
3    fmt.Println(&b[1])//0xc00001e128 slice 可以
  1. recover的使用只能在 defer中使用(其它地方调用无效果)
代码语言:javascript
复制
1func tt(){
2defer func(){
3if t := recover;t {
4    fmt.Print(t)
5}
6}
7dd()//dd里有panic
8}
  1. 关于接口类型的断言
  • 接口实例.(接口类型)
  • 接口实例.(实际类型)

但是这两个的前面 无一例外都需要传入实际的类型也就是变成了

  • 实际类型的实例.(接口类型)
  • 实际类型的实例.(实例类型)

举个例子

代码语言:javascript
复制
 1type a struct {
 2  value string
 3}
 4
 5type b interface {
 6  get()
 7}
 8type c interface{
 9  post()
10}
11
12func tt(b1 b){
13  // 第一种情况
14  if v,ok := b1.(c);ok {// 这个实例 相当于实现了这两个interface
15    fmt.println(v.post())
16    fmt.Println(v.get())
17  }
18
19  // 第二种情况
20  if v,ok := b1.(a);ok {
21    fmt.Println(v.get())
22  }
23}

再看一个实际运用上的例子:

代码语言:javascript
复制
 1type a struct {
 2    value string
 3}
 4
 5type ber interface {
 6    get()
 7}
 8
 9type cer interface {
10    post()
11}
12
13func (a1 a) get() {
14    fmt.Println(a1.value)
15}
16
17func (a1 a) post() {
18    fmt.Println(a1.value + "p")
19}
20
21func t(b1 ber){
22  // 这个内部的cer必不可少。
23    type cer interface {// 这就是为了验证 已经实现了ber的变量是否也实现了cer
24        post()
25    }
26    if v, ok := b1.(cer); ok {// 这个地方隐藏的说明了  a的实例是满足ber的,不然它这一步就会panic然后它还得满足cer不然还会panic所以这一步直接验证了两次。
27        v.post()
28    }
29    b1.get()
30
31}
  1. 关于递归, 递归其实就是在执行函数里的函数,直到所有函数都结束了,然后就结束即可。举个例子
代码语言:javascript
复制
1func testVisit(ii int) int {
2    if ii == 0 {
3        return 100
4    }
5    fmt.Println(ii)
6    ii = testVisit(ii - 1)
7
8    return ii+1
9}

它的执行很明显是从外层的初始栈开始往里执行,然后所有栈执行完毕即可,这里我使用了ii = testVisit(ii -1) 目的有两个,1 为了让每下一个的ii都少1,2 就是为了获取上一个栈的返回值,然后每次返回都+1 最后的返回值是109 这也证明了每次返回都是从最上层的栈开始往下调用然后到最下面的然后返回。

  1. 关于 channel

chan 的机制是这样的,当一个没有缓存的(有缓存也是一样只是当缓存满了就一样了)chan,显示导入一个数据,这个时候 这个发送chan的goruntine就睡眠了(阻塞)然后直到这个chan被接受(只要被接收就行,不管是不是在同样一个goruntine)然后这个数据就被获取了,然后开始唤醒这个chan的发送者的那个goruntine。如果没有后续的数据那么这个chan就应该被关闭了可以人工关闭(close)也可能被系统收回。

看一个例子 这是一个有缓存的,并且利用缓存来限制 http请求数量的操作

代码语言:javascript
复制
 1var st = make(chan struct{},20)// 将访问的数据限制在20
 2var sy synv.WaitGroup
 3func main(){
 4  dd := []string{"htps://...",",,,,,",",,,"}
 5  sy.Add(len(dd))
 6  for _,v := range dd {
 7
 8      go read(dd)//
 9  }
10
11sy.Wait()
12fmt.Println("执行完毕")
13}
14func read(st){
15  defer sy.Done()
16  st <- struct{}// 因为是有缓存的chan所以可以保证一直有20个gorutine是不阻塞的。
17  // 只要有一个goruntine不是阻塞的就不会造成死锁
18  rea(st)
19  <- st
20}
21func rea(st string){
22  res,err := http.Get(st)
23}

只要有一个goruntine不是阻塞的就不会造成死锁,死锁是程序想退出,但是chan内还有东西,没办法退出,但是又没办法运行,造成了无法结束的窘迫,最终就是各个goruntine都是阻塞然而又不能退出的局面。总之 死锁问题有必要再开一个文件来讨论一下。

  1. 关于 type

alias的类型和底层可以转化但是不是隐式是显式。

这里分几个内容

  • 一就是
代码语言:javascript
复制
1type hand func(http....,http.....)
2
3// 例如
4
5httprouter.handle("/",httprouter.handle)
6// 这个时候就是
7httprouter.Handle("/",func(http....,http....))
8// 即可。
9

这种类型的尤其是在函数的调用的时候 要满足 一个hand类型也是很简单 就是函数满足后面那个样式即可

  • type hand string

这种情况也是 函数满足后面的那个 type即可 也就是 是string即可 。

  1. 关于引用类型

举个 slice说明一下

代码语言:javascript
复制
 1func main(){
 2  t := make([]int,0,10)
 3  t = []int{
 4    "12",
 5  }
 6  visit(t)
 7  fmt.Println(t)
 8}
 9
10func visit(t []string){
11  t = append(s,"1221")
12}

猜一下 输出的是什么? 是 ["12","1221"]吗? 我本来一直以为是,后来我发现其实不是,我们要先证实一个问题,引用类型并不是指针,它是一个数据结构通常是 一个cap 一个len和一个指针对象。所以它本身也是一个实际的值。当这个地方把t传入visit后,其实是值的复制,然后在visit中,t等于了一个append返回的一个新的slice,那么它就不是指向了原来的那个底层数组了,(换言之,这样的话就不是改变底层数组了,是重新分配了一个数组,那么原来的那个slice自然就跟这个新的底层数组没有关系了)那么什么时候会改变呢,也很简单

代码语言:javascript
复制
1func visit(t []string){
2  t [1] = "112"
3}

这就叫做赋值,它是直接操控底层的数组进行了值的改变,这并没有去进行值的拷贝或者是指针传递。到这里我么可以说一下了 在go里所有的类型只要是传递数据只有两种模式,1 值的拷贝(包括引用类型它的拷贝只不过是拷贝的它的数据组织)2 指针的拷贝,说白了,指针的拷贝也是值的拷贝,因为它本身也是一种值只不过象征了一种钥匙罢了,所以,除了对数据本身进行直接改变,改变他的数据本身,这种行为可以改变它自己,值的传递的话 统统是有拷贝行为。所以我们以后不能把引用类型看成指针,很不一样。如果是指针的话 那么肯定必须要获取值才能去改变,但是引用类型是go的编译器自动的行为。(例如扩容啊,自动获取底层数据的值啊这种)

  1. 关于带(bare return)形参及不带形参的返回值和defer的故事

看两个例子:

之前的例子说过了,defer只是执行滞后但是参数记住是参数也就是将形参传入实参的过程其实是同步的并没有什么区别。

代码语言:javascript
复制
 1//  这个例子中返回值是1
 2func tt()(t int){
 3    defer func() {
 4        t ++
 5    }()
 6    return
 7}
 8
 9// 这个例子中返回值是0
10func tt1()int{
11    t := 0
12    defer func() {
13        t ++
14    }()
15    return t
16}
17// 0 这个例子证明了 参数顺序执行化。
18func tt1()int{
19    t := 0
20    defer func(t int) {
21        t ++
22    }(t)
23    return t
24}

原因也是很简单,首先如果是没有形参的返回值,都是在return后面直接返回的,然后再执行defer然后再执行 os.exit() 但是有形参的就不一样了,它必须返回它形参定义的参数之歌例子中就是t,那么t在哪最后一个出现呢?就是在defer中,所以它的执行过程就变成了,找寻最后出现的t(这里出现在defer中)然后直接执行os.exit() 因为它return后面没有东西,所以它和没有形参的return XXX 很不一样。 那么如果是这样的呢?

代码语言:javascript
复制
1func age()(n int,err error){
2  return
3}

它会有什么返回结果呢?答案就是0 nil ---- 如果只有return 但是却没有出现n和err那么简单 返回值里不是已经初始化了嘛,那么久返回初始化的结果不就好了嘛所以是 0 nil(他们的初始化值)

  1. 关于buffered

我们在go的执行中经常使用的一种技巧就是限制go并发的速度,那么这个时候buffered变量就可以实现了它的实现是这样的 make(chan xxx,number) 在get请求中一般我都会这么使用make(chan struct{},20) 我们定义了一个新的类型就是 struct{} 这个类型是代表了空,当然你也可以使用bool 都可以 struct{} 类型使用的时候 用 struct{}{} 即可。这就代表了这个chan中最多可以暂存number个数据,这就是所谓的缓存技术,也叫做 buffered数据

  1. 关于 recover和并发(多goruntine)

如果是在go的多协程中的panic一定要在这个协程中recover否则在主协程的recover根本无法获取这个panic

代码语言:javascript
复制
 1go func(i int) {
 2            defer sy.Done()
 3            defer func() { // 如果是在外部获取recover可以说压根获取不了,想想也是知道的因为你并不知道主协程和这个协程到底哪个运行到哪了,所以要在这个协程中搞定这个panic
 4                if e := recover();e != nil {
 5                    fmt.Println(e)
 6                }
 7            }()
 8            start := time.Now()
 9            resp, err := http.Get(url[i])
10
11            if err != nil {
12                fmt.Println(err)
13            }
14            n, err := html.Parse(resp.Body)
15            if err != nil {
16                fmt.Println("err",err)
17                return
18            }
19            defer resp.Body.Close()
20            if err != nil {
21                fmt.Println(err)
22            }
23            wor, im := countWordsAndImagesAsync(nums, ch, n)
24            ma.Store(url[i]+"   num", wor)
25            ma.Store(url[i]+"   image", im)
26            end := time.Now()
27            timeS := end.Sub(start)
28            ma.Store(url[i]+"花费的时间是:",timeS.String())
29        }(i)
  1. 关于递归的出栈和进栈 递归都有一个进出栈的过程,
代码语言:javascript
复制
 1func a(){
 2  visit(start,end)
 3}
 4func visit(start,end func()){
 5start()//在进栈时执行的函数
 6  for {
 7    visit()
 8    if XXXXXX 然后退出这个栈开始出栈
 9  }
10  end()// 在出栈时执行的函数。
11}
  1. 关于 函数内部的函数
代码语言:javascript
复制
 1func t(){
 2  var d func()int // 使用这种方式一般都是函数内部有递归,如果不实现 声明一下 函数内部的递归函数将无法运行
 3
 4  d = func()int{
 5    //fdffd
 6  }
 7  d()
 8
 9  // 或者
10var d = func(){
11
12}
13d()
14
15总之,不能使用
16func()int{
17
18}
19int() 在go语言中这种行为不允许
20
21}

关于函数内部声明类型 倒是很随意

代码语言:javascript
复制
1func t(t1 inter){
2  type t struct{
3    get()
4  }
5  if d,ok := t1.(t);ok {
6    d.get()
7  }
8  t1.post()
9}
  1. 只有接口和nil不能拥有方法。
代码语言:javascript
复制
1invalid receiver type io.Writer (io.Writer is an interface type) // 这是使用了接口的报错。
2
3nil is not a type// 这是使用了nil的报错

ps: 永远不要去取接口的指针,没有丝毫的意义。如果取 slice的指针还有些许的意义(比如在append的时候)但是接口的指针有什么意义? 接口本来就没有实际的意义它本来就是一个抽象的东西。而且它本来也就是引用对象。

  1. time.After 的用法

它的作用是 当这个系统没有东西了,然后在设置后几秒后运行,如果其他的case一直有东西,那么它是不会被执行的。 因为在select选择的时候只有在其他的case都没有反应了的时候才会去选择time.After 所以它可以用在 比如什么东西都没有数据了以后 然后按照某时间后去取消这个东西,当然还有一种应用场景就是,无论如何就是要5分钟后取消,打死都要 那么可以 使用两个select,有一个select就放一个after和一个default即可。

代码语言:javascript
复制
 1select{
 2case time.After():
 3return // 这样就强制 退出了
 4default:
 5
 6}
 7
 8select {
 9// 这个select就是干正事的。
10}

或则,使用 context包的withcanceltimeout 这个函数厉害 无论如何 只要设置的分钟数到了,就能立马取消。 因为cancle withcancle那个函数 如果 不执行cancle函数 那么ctx.done 就无法运行,这个时候 cancle的关闭就要在执行这个有ctx的函数之前了。就不能使用defer函数来关闭这个,因为一直有东西运行。

  1. 关于 string字符串 []byte 以及[]byte的十六进制表示(以string形式储存)
代码语言:javascript
复制
 1   // 将string字符串,以unicode编码的形式,找到所有的字符的unicode表示,然后返回位一个数组。
 2    // [72 101 108 108 111] 就是这个数组(slice)
 3    src := []byte("Hello")
 4    // 这个encodeStr 是什么呢?它其实就是把这个数组的所有的数字用16进制表示并且没有加[]而已,而是将这个串变成了字符串的形式储存
 5    // 就是这个“48656c6c6f” 这个字符串其实还是unicode编码只是 用的16进制并且没有[]罢了,一定不要认为它就是"HELLO"
 6    encodedStr := hex.EncodeToString(src)
 7    fmt.Println(src)
 8    // 48656c6c6f -> 48(4*16+8=72) 65(6*16+5=101) 6c 6c 6f
 9    fmt.Println(encodedStr)
10    byteValue,_ := hex.DecodeString(encodeStr)
11    string(byteValue) == "Hello"
  1. 关于json的一个解析的问题
代码语言:javascript
复制
1{"code":0,"data":{"ip":"173.82.115.125","country":"美国","area":"","region":"加利福尼亚","city":"洛杉矶","county":"XX","isp":"XX","country_id":"US","area_id":"","region_id":"US_104","city_id":"US_1018","county_id":"xx","isp_id":"xx"}}
代码语言:javascript
复制
1type Data struct {
2    Data Values `json:"data"`
3}
4type Values struct {
5    Country string `json:"country"`
6    City    string `json:"city"`
7}

定义的时候可以缺少字段,但是,不能跟json字段的格式不符合,举个例子 这里的数据是 在json整个文件下的data对象中,那么你需要两个struct 一个是代表整个的json的数据,第二个struct是代表那个data,你看 那个code和其它字段没有定义吧,没有定义无所谓,但是字段的格式一定要遵守 如果直接把Values传进去就是错误的行为,是不会解析的。

  1. 关于go template

第一点 如果你使用template.Execute 那么你在那个最外边的layout那个文件里不能使用{{define "layout"}}{{end}} 如果你想使用{{define "layout"}}{{end}} 那么 你需要tem.ExecuteTemplate(w,"layout",nil)那个中间的变量要用最外边的那个模块 所以最好的就是最外边的那个不用模块,然后使用那个没有模块的就ok了

第二点 如果如果你的子模块就是小的模块很多人称作是母模块 我当他们是小模块子模块,他们里面有变量,那么你肯定是最后使用的是layout这个最外面的文件或者说是模块 那么你就要{{template "son".}} . 看到了吗 这个点没有这个点 你在最外面的模块也就是最终使用的时候你发现你的变量压根没有导入,这个就是 变量导入的标志 也就是是 你的子有了 如果不导入 那么这个数据就消失了,我被这个地方坑了几个小时。我的天~~~~。


版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-02-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档