V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
suyuanhxx
V2EX  ›  分享创造

Java 程序员学习 Go 指南

  •  3
     
  •   suyuanhxx ·
    suyuanhxx · 2018-04-24 13:47:37 +08:00 · 5841 次点击
    这是一个创建于 2405 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Java 程序员学习 Go 指南

    从事 java 编码也有三年多了,写的Java代码也很多。Go语言,我是无意间接触到的。在去年 12 月份左右的时候比特币大涨到 1w 刀,就想着研究下比特币,而同时有听说 Go 语言在区块链中非常火爆。就抱着学着看看的心情了解了Go,不知不觉喜欢上Go语言的简洁和优雅了。
    Go语言是谷歌推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。
    Go语言的高并发支持,语法的简洁性,指针的自动垃圾回收,可以让开发人员将精力放在业务处理上。Go语言能够将开发人员带入更生层次的去了解底层操作系统,而不仅局限于语言本身。Go语言的"Less is more",需要在编码过程中去体会这种设计哲学
    以数据交换为例子,Go 语言实现:
    array[i] , array[j] = array[j] , array[i] // Go
    Java实现:

    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
    

    语法

    在语法上Go相对Java有着很多开发人员非常喜欢的特性。可以说这些特性,设计的非常人性化。大大简化了开发人员的工作。语法上从以下四点介绍JavaGo区别

    1. 变量,赋值操作
      在变量命名规则上JavaGo基本相同,都是大小写字母数字,下划线,不能以数字开头。但在Go中以大写字母开头的变量、方法、函数、结构体都表示public(对所有类可见)类型,小写表示private(本类可见)类型(这里不讨论下划线开头)。而Java中用public private关键字明确表示,没有明确表示的为default(包可见)类型,有比较严格的访问级别。Go语言特意淡化了此点,可以节省很多代码。但也为初学者留下了一些小坑,比如JSON序列化时,属性字段必须大写才能序列化。
      例如:(Go语言以换行符表示一行代码结束,Java;表示结束,此法对Go依然适用)

      type Response struct {
          Message      string        `json:"message"`//将 Message 字段序列化成 JSON 中的 message
          userName     string        `json:"userName"`//首字母小写,无法被序列化
      }
      

      在变量声明赋值上,Go语言采用现代化“脚本语言”声明方式。Go支持类型自动推断,故在声明变量时可以将变量类型省略。以下一组代码说明变量a b c d的多种声明赋值方式,当多个变量赋值时推荐使用最后一种方式,代码量最少。相对于Java一行代码只能声明一个变量,要便捷很多。
      函数外部申明必须使用var,不要采用:=

      var a int
      a = 10
      var b int = 10
      var c = 10
      d := 10
      var a, b, c, d = 10
      a, b, c, d := 10
      a, b, c, d := 10, 10, 11, 12
      
    2. 数据类型
      Go语言中的数据类型与Java差距比较大,Go常用基本数据类型继承了C/C++语言特点。
      Java中有包装类型,包装类型为对象,Go没有此功能。Go中还包含一些其他的数据类型简写: byte => uint8int => uintuint => uint32uint64rune => int32uintptr表示无符号整型,用于存放一个指针。
      下面表格具体表示了GoJava数据类型对比:

      Go的数据类型比Java更加丰富,但使用起来上两者相差不多。Go中没有包装类型,大大减少Java中思考使用包装类型还是简单类型的时间。Go中也引入了无符号,有符号数据类型的选择。
      在处理中文字符上,GoJava一样方便。

    3. 指针,slicemap

      • 指针
        Go语言仍采用了C/C++中的指针,相对于Java稍显复杂。但由于Go中能够自动垃圾回收,只需要学会灵活使用指针就能够大幅减少内存操作时间,简化代码。可以说 Go 语言指针是C/C++Java的中合。能够享受指针的便捷,同时又省去手动释放指针的麻烦。 指针变量可以指向任何一个值所在的内存地址。Go中使用*来声明一个指针,同时也是取指针值操作符。&用来取内存地址,16 进制,例如0xc420014608,不同的机器,不同的环境内存地址都会不同。
        var p *int//声明一个指针
        a:=10
        p=&a //指针赋值
        fmt.Println(p)  //打印指针所指向内存地址 0xc420014608
        fmt.Println(&a) //a 与 p 所指向内存中同一地址 0xc420014608
        fmt.Println(&p) //取变量 p 地址,会变, 0xc42000e048
        fmt.Println(*p) //打印指针内容, 10
        
      • slice
        sliceJava中的List很像,都是能够自动扩容的数组集合。GoList实现了栈和队列的功能,和Java List差距比较大。在结构上Go slice为动态素组,可以自动根据当前元素和声明的容量进行扩容。使用起来非常方便,但可能会带来性能的消耗,频繁的扩容将使代码运行速率下降。 slice有两个额外的属性:len(长度),capacity(容量), var c = make([]int, capacity)
        同时需要注意由数组创建的slice为数组引用,改变slice的值会导致原数组也会发生变化。这种现象在Go中非常常见,使用时要格外注意。
        var a [4]int      // 数组
        a = [4]int{1, 2, 3, 4}// 数组赋值
        b := a[1:3] //slice,数组 a 的引用,从 a 数组下标 1 到下标 2
        b[1] = 5 //a={1,2,5,4}, b={2,5}
        
        而在Java中其实也是存在这种现象的,不过 Java 中“一切皆是对象”的原则,我们在创建一个新的变量或者对象时,都是使用 new 操作,然后再赋值,所以这种对象引用的该变导致原对象也被改变的情况比较少。变量作为参数时,这种情况发生较多,以下 Java 代码也时有发生:
        List<Integer> a = new ArrayList<>();
        a.add(1);
        a.add(2);
        List<Integer> b = a;
        b.add(5);
        
        slice有两个非常重要的函数copy()append()append函数用于给slice追加元素,当 slice 容量不够时会自动扩容,例如b = append(b, 3)copy函数用于将一个数组拷贝到另一个数组,不会自动扩容,例如:
        copy(b, []int{7,3,8}) // b={7,3},由于 b 的长度只有 2,copy 只会复制前两位
        
      • map
        mapJava中的HashMap就设计思想有很多相似的地方,都是非线程安全的数据结构。JavaHashMap底层采用数组(bucket)+链表+红黑树(红黑树为JDK1.8新增部分)的数据结构。都是考虑到Map的使用场景,牺牲线程安全提升访问效率的实现方式。并发情况下使用ConcurrentHashMapGo map使用方式相比Java更为简单,任何数据类型都可以作为keyJavakey必须为对象,基本数据类型的封装类型才能作为 key ),例如:
        var m map[int]string  // 声明
        m[1]="1"              // 赋值
        fmt.Println(m[1])     // 取值
        
        如何用 Go 实现一个线程安全的 map ?最直接的方式就是在 map 读写的时候加上读写锁...
        type ConcurrentHashMap struct {
            lock *sync.RWMutex //Read and write Lock
            cm   map[interface{}]interface{}
        }
        func (m *ConcurrentHashMap) Get(k interface{}) interface{} {
            m.lock.RLock() //Read Lock
            defer m.lock.RUnlock()
            if val, ok := m.cm[k]; ok {
                return val
            }
            return nil
        }
        func (m *ConcurrentHashMap) Set(k interface{}, v interface{}) bool {
            m.lock.Lock() // Write Lock
            defer m.lock.Unlock()
            if val, ok := m.cm[k]; !ok {
                m.cm[k] = v
            } else if val != v {
                m.cm[k] = v
            } else {
                return false
            }
            return true
        }
        
        Go maphash结构的,意味着平均访问时间是 O(1)的。同传统的hashmap一样,由一个个bucket组成,bucket内部又由一个指针数组组成。按key的类型采用相应的 hash 算法得到keyhash值。将hash值的低位当作 Hmap 结构体中 buckets 数组的 index,找到 key 所在的 bucket。将 hash 的高 8 位存储在了buckettophash中。注意,这里高 8 位不是用来当作key/value 在bucket内部的offset的,而是作为一个主键,在查找时对tophash数组的每一项进行顺序匹配的。先比较hash值高位与buckettophash[i]是否相等,如果相等则再比较bucket的第i个的key与所给的key是否相等。如果相等,则返回其对应的value,反之,在overflow buckets中按照上述方法继续寻找。
        Hmap 结构图
        数组,slicemap的遍历都可直接采用range关键字,foreach的形式遍历,也可采用for循环方式遍历。foreach遍历和Java相同,都不宜在遍历过程中改变原值。
    4. 方法函数
      对于习惯了 Java 的开发人员来说,总是会将方法和函数当成一个概念,但在 Go 中这两个确有一些不同之处。但形式差不多,功能相同。

      • 函数,由关键字func定义,完成特定功能,可以有多个返回值。例如:func Add(a int, b int) int { ... }。形式上和 Java 有一定差距,功能作用和 Java 一样。不过 Go 中能够有多个返回值,这极大的简化了实际功能的完成。一下几个例子说明方法的声明:
      func Add(a int, b int) int { ... }
      func Sub(a int, b int) (result int) { ... }// result 为返回对象,可在方法体中对 result 直接赋值
      func Multi(a int, b int)  (int, string) { ... } //多个返回值必须是用括号
      func Divide(a int, b int) (result int, s tring) { ... }//如果多个返回值中其中一个有返回值变量,其他的也要有返回值变量
      
      • 方法,函数和方法的定义相差不大。函数一般都是继承某个接口而来,相当于 Java 中某个类中定义的方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。例如:
        func (user *User) getUserName() string { ... }示例中的User为一个结构体,getUserName为结构体的一个方法。
    5. 面向对象
      Go 中面向对象要简单很多,去除了 Java,C/C++中复杂的继承关系,保留了接口interfacestructinterface中包含的是方法,struct中只能定义属性。strcut能够实现interface中的方法(函数)。两者相结合使用实现面向对象的思想,相比 Java 和 C/C++简洁了不少,概念也减少了很多。面向对象的思想还在,仍存在对象关联(父子类)关系。

          type People interface {
              getUserName() string
          }
          type User struct {
              Name string
          }
          func (user *User) getUserName() string {
              return user.Name
          }
          func main() {
              var p People
              p = &User{Name: "zhangshan"} //使用 User 初始化 p,p 为接口 People 的实现,只包含方法 getUserName()
              fmt.Print(p.getUserName())  // print "zhangshan"
          }
      

      Go代码中我们要防止接口的滥用,相比Java中让人头疼的继承和接口实现,Go要相对简单,但我们在使用interface时需要考虑是否必要。

    错误处理

    Go语言中的错误处理相比Java中的try catch要相对简单一点,但从另一方面 Go 中的Errors are values却又麻烦很多!习惯了Java中的try抛出异常,catch中处理异常的开发人员来说,可能对于Go中的errorpanic处理方式会有点不能适应。
    Java中我们通常处理异常(错误)的方式为:将一段可能出错的业务代码使用try catch包裹。try中进行正常业务逻辑,在catch模块对业务逻辑进行补偿操作,事务回滚等,在finally模块对资源进行释放。看起来是一段相对严谨的处理逻辑,大多数开发人员处理到这就结束了。但这其中却包含很多不确定性,请看如下代码:

    try{
        A.close();
    }catch(Exception e){
        try{
            B.close();
        }catch(Exception e){
            B.close();
        }finally{
            B.close();
        }
    }finally{
        try{
            C.close();
        }catch(Exception e){
            C.close();
        }finally{
            C.close();
        }
    }
    

    对于不确定的值,开发人员都会尝试去捕捉,并做出处理,但上述冗长的代码,感觉非常糟糕,最终的情况可能是 ABC 三个链接都没法正常关闭。Javatry catch给我们代码方便的同时,却留下很大的“操作空间”,这可能就陷入了一个死胡同。偷懒的开发人员可能会直接放弃处理,而且大多数开发人员都会如此。因为这实在是太冗长了。在实际开发过程中,catch 中往往只做了打印异常的功能,很多开发人员补偿都不会做!
    Go就将这种情况摆在开发人员的面前(Java中开发人员能够睁一只眼闭一只眼),你无法忽视这个问题。Goerror设计是,错误也是一种合法的值——“ Errors are values ”

    err := Sub()
    if err != nil { //与常规处理思路相反,优先处理错误
        fmt.Print(err)
        return err
    }
    ...//正常业务逻辑
    

    Go中的异常panic会导致整个Go程序 crash (进一步说明Go中的异常必须处理,不可忽略),防止异常导致整个程序崩溃,我们要使用recover()来进行恢复。 同时引入关键字defer来延迟执行 defer 后面的函数,非常适合用来处理异常和错误。多条 defer 函数的处理顺序和声明顺序相反。Go 中有很多正确处理错误的实践方式,需要在实际编码过程中体会。

    并发

    Go 的并发模型设计来自 CSP 模型。Golang 借用 CSP 模型的一些概念为之实现并发进行理论支持,其实从实际上出发,go 语言并没有完全实现了 CSP 模型的所有理论。仅仅是借用了 process 和 channel 这两个概念。相对于 Java 的并发设计,使用起来要方便很多,因此 Go 可以轻易的起成千上万个协程( Goroutine )。(这里并不会讲述 Go 调度器模型)
    Goroutine 是实际并发执行的实体,它底层是使用协程(coroutine)实现并发。coroutine 是一种运行在用户态的用户线程,类似于 greenthread,go 底层选择使用 coroutine 的出发点是因为,它具有以下特点:

    • 用户空间 避免了内核态和用户态的切换导致的成本
    • 可以由语言和框架层进行调度
    • 更小的栈空间允许创建大量的实例

    goroutine 是在 golang 层面提供了调度器,并且对网络 IO 库进行了封装,屏蔽了复杂的细节,对外提供统一的语法关键字支持,简化了并发程序编写的成本。Go 中并发编程示例:

    1. 使用channel控制并发,channel又分为带缓冲buffer和不带缓冲buffer。不带缓冲的 channel 不能在同一个 gorutine 读写,否者发生死锁。带缓冲的 channel 可以。
      func main() {
          ch := make(chan int) // 声明一个不带缓冲的 channel,ch := make(chan int,2) 带缓冲 channel,缓冲为 2
          go func() {   // go 关键字表示启动一个协程 goroutine
              ch <- 1   // channel <- 表示向 channel 中写数据
          }()
          fmt.Println(<-ch) // i:=<- channle 表示读取 channel 中的数据到变量 i
      }
      
    2. 使用sync.WaitGroup控制并发
      fun main(){
          var wg sync.WaitGroup
          var urls = []string{"http://www.golang.org/", "http://www.google.com/"}
          for _, url := range urls {
              wg.Add(1)     //每有一个 goroutinie,wg+1
              go func(url string) {
                  defer wg.Done()   // 函数最后将该协程 goroutine 标记为完成
                  http.Get(url)
              }(url)
          }
          wg.Wait()  // 等待所有的 goroutine 完成后再执行下面的代码
      }
      
    3. 使用context实现并发控制。context主要是用来处理goroutine中又开启其他gourine,达到跟踪goroutine的解决方案。主要是用来处理多个goroutine之间共享数据,及多个goroutine的管理。

    垃圾回收

    Go语言有指针,也有自动垃圾回收。这一点上与Java一致,都采用了标记清除算法,不过Go中还有另外两种垃圾回收算法:位图标记和内存布局,精确的垃圾回收。
    讨论垃圾回收,就需要知道为什么要有垃圾回收,那就需要先了解系统是如何分配内存。操作系统中有一个内存池。首先,它会向操作系统申请大块内存,自己管理这部分内存。然后,它是一个池子,当上层释放内存时它不实际归还给操作系统,而是放回池子重复利用。这样反复的过程中,内存管理中必然会出现内存碎片问题,当代码中需要申请一个较大的对象时,原用的碎片空间已经不够使用,这就出现了垃圾回收。
    垃圾回收有着非常长的历史,第一批垃圾回收算法是为单核机器和小内存程序而设计的。那个时候,CPU 和内存价格昂贵,而且用户没有太多的要求,即使有明显的停顿也没有关系。这个时期的算法设计更注重最小化回收器对 CPU 和堆内存的开销。也就是说,除非内存不足,否则 GC 什么事也不做。而当内存不足时,程序会被暂停,堆空间会被标记并清除,部分内存会被尽快释放出来。
    分代理论假说,大部分的内存对象“朝生夕死”,它们在分配到内存不久之后就被作为垃圾回收。这就是分代理论假说的基础,它是整个软件产品线 领域最贴合实际的发现。数十年来,在软件行业,这个现象在各种编程语言上表现出惊人的一致性,不管是函数式编程语言、命令式编程语言、没有值类型的编程语言,还是有值类型的编程语言。现代垃圾回收器基本上都是基于分代算法。分代回收器可以加入其它各种特性,一个现代回收器将会集并发、并行、压缩和分代于一身。
    例如 Java 中 JVM 的 GC 分为“年轻代”和“老年代”,“年轻代”的对象大多“朝生夕死”,能够存活下来的对象会放到老年代中。是典型的分代回收器。
    下面简单分析集中垃圾回收算法:

    1. 标记清除算法
      我们都知道标记清除算法会“ stop the world ”。内存单元并不会在变成垃圾立刻回收,而是保持不可达状态,直到到达某个阈值或者固定时间长度。这个时候系统会挂起用户程序,也就是 STW,转而执行垃圾回收程序。
      该算法中有一个标记初始的 root 区域,以及一个受控堆区。root 区域主要是程序运行到当前时刻的栈和全局数据区域。在受控堆区中,很多数据是程序以后不需要用到的,这类数据就可以被当作垃圾回收了。判断一个对象是否为垃圾,就是看从 root 区域的对象是否有直接或间接的引用到这个对象。如果没有任何对象引用到它,则说明它没有被使用,因此可以安全地当作垃圾回收掉。
      标记清扫算法分为两阶段:标记阶段和清扫阶段。标记阶段( Go 采用的是三色标记法),从 root 区域出发,扫描所有 root 区域的对象直接或间接引用到的对象,将这些对上全部加上标记。在回收阶段,扫描整个堆区,对所有无标记的对象进行回收。)more

    2. 位图标记和内存布局 既然垃圾回收算法要求给对象加上垃圾回收的标记,显然是需要有标记位的。一般的做法会将对象结构体中加上一个标记域,一些优化的做法会利用对象指针的低位进行标记,这都只是些奇技淫巧罢了。Go 没有这么做,它的对象和 C 的结构体对象完全一致,使用的是非侵入式的标记位.

    3. 精确垃圾回收
      通过定位对象的类型信息,得到该类型中的垃圾回收的指令码,通过一个状态机解释这段指令码来执行特定类型的垃圾回收工作。对于堆中任意地址的对象,找到它的类型信息过程为,先通过它在的内存页找到它所属的 MSpan,然后通过 MSpan 中的类型信息找到它的类型信息。more

    微服务架构

    Go作为一个现代化后端程序语言,搭建微服务架构当然也是没有任何问题的。Go 通常采用Go-Kit作微服务框架,Java通常采用Spring Cloud做微服务架构。
    微服务的架构主要关键包含以下几点:

    1. 服务拆分
      服务拆分粒度主要看具体业务需求,不易太宽泛,也最好不要太细,不然就会产生几十个微服务,维护成本较大。

    2. 服务治理(服务注册发现)
      服务注册发现又分为两种:

      • 客户端发现模式
        当使用客户端发现模式时,客户端负责决定相应服务实例的网络位置,并且对请求实现负载均衡。客户端从一个服务注册服务中查询,其中是所有可用服务实例的库。客户端使用负载均衡算法从多个服务实例中选择出一个,然后发出请求。服务实例的网络位置是在启动时注册到服务注册表中,并且在服务终止时从注册表中删除。服务实例注册信息一般是使用心跳机制来定期刷新的。Netflix Eureka 是一个服务注册表,为服务实例注册管理和查询可用实例提供了 REST API 接口。Netflix Ribbon 是一种 IPC 客户端,与 Eureka 合同工作实现对请求的负载均衡。
      • 服务端发现模式
        客户端通过负载均衡器向某个服务提出请求,负载均衡器向服务注册表发出请求,将每个请求转发往可用的服务实例。跟客户端发现一样,服务实例在服务注册表中注册或者注销。

      Java 微服务实践中通常使用Spring Cloud框架,使用Eureka作为微服务的服务注册表,spring 封装了Eureka,让 Eureka 即作服务转发又作服务注册表。
      Go 中可以使用Consul(一个用于发现和配置的服务。提供了一个 API 允许客户端注册和发现服务。Consul 可以用于健康检查来判断服务可用性),etcd(一个高可用,分布式的,一致性的,键值表,用于共享配置和服务发现。两个著名案例包括 Kubernetes 和 Cloud Foundry)

    3. 远程调用 RPCRPC方面JavaGo均可采用GRPCApache thrift或者直接采用Restful风格的Http请求。Java也可采用dubbo封装的RPC方式

    4. 高可用,负载均衡
      服务治理能够在服务与服务之间时间负载均衡。 HTTP反向代理和负载据衡器(例如NGINX)可以用于服务发现负载均衡器。服务注册表可以将路由信息推送到NGINX,激活一个实时配置更新;例如,可以使用 Consul TemplateNGINX Plus支持额外的动态重新配置机制,可以使用DNS,将服务实例信息从注册表中拉下来,并且提供远程配置的API

    5. 网关,路由追踪
      理论上说,一个客户端可以直接给多个微服务中的任何一个发起请求。每一个微服务都会有一个对外服务端(https://serviceName.api.company.name)。这个 URL 可能会映射到微服务的负载均衡上,它再转发请求到具体节点上。
      通常来说,一个更好的解决办法是采用API Gateway的方式。API Gateway是一个服务器,也可以说是进入系统的唯一节点。这跟面向对象设计模式中的 Facade 模式很像。API Gateway封装内部系统的架构,并且提供 API 给各个客户端。它还可能有其他功能,如授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理等。
      Java中通常使用zuul来搭建服务器网关。


    最后给大家推荐我的几个 Repo

    第 1 条附言  ·  2018-04-24 20:22:03 +08:00
    int6 -> int8
    go 中没有 int6 类型
    21 条回复    2018-04-28 18:23:27 +08:00
    suyuanhxx
        1
    suyuanhxx  
    OP
       2018-04-24 13:48:43 +08:00
    不知道为什么表格显示错误...蛋疼
    Durandcol
        2
    Durandcol  
       2018-04-24 14:55:02 +08:00
    执行力好高啊.. 同去年炒币
    今年 1 月份看了看 go tour 就没再学习了...
    suyuanhxx
        3
    suyuanhxx  
    OP
       2018-04-24 15:02:31 +08:00
    @Durandcol #2 哈哈
    kindjeff
        4
    kindjeff  
       2018-04-24 17:32:27 +08:00   ❤️ 1
    看完了,不错的科普
    suyuanhxx
        5
    suyuanhxx  
    OP
       2018-04-24 17:40:23 +08:00
    @kindjeff #4 嗯,写的还是比较简单的
    mifly
        6
    mifly  
       2018-04-24 19:28:35 +08:00 via Android
    写的不错,同写过过 Java,go,c,还是喜欢 go 多点
    suyuanhxx
        7
    suyuanhxx  
    OP
       2018-04-24 19:30:00 +08:00
    @mifly #6 厉害厉害
    ebony0319
        8
    ebony0319  
       2018-04-24 19:55:39 +08:00 via Android
    收藏,地铁上看。
    rails3
        9
    rails3  
       2018-04-24 20:11:32 +08:00
    int8 写成了 int6
    suyuanhxx
        10
    suyuanhxx  
    OP
       2018-04-24 20:21:26 +08:00
    @rails3 #9 多谢更正
    mingyun
        11
    mingyun  
       2018-04-24 23:38:32 +08:00
    有其他语言学 go 的指南吗?比如 PHP
    jiangnanyanyu
        12
    jiangnanyanyu  
       2018-04-24 23:49:19 +08:00 via Android
    mark 一下
    fanjianhang
        13
    fanjianhang  
       2018-04-25 00:03:34 +08:00 via Android
    mark
    wwuha
        14
    wwuha  
       2018-04-25 09:15:28 +08:00
    mark
    kangkang
        15
    kangkang  
       2018-04-25 12:06:26 +08:00
    期待楼主写一个 GO 程序学习 java 指南
    suyuanhxx
        16
    suyuanhxx  
    OP
       2018-04-25 12:15:14 +08:00
    @kangkang #15 :doge)
    jrient
        17
    jrient  
       2018-04-25 14:16:29 +08:00
    mark
    haidaochuan14
        18
    haidaochuan14  
       2018-04-26 14:38:46 +08:00
    mark 一下
    abmin521
        19
    abmin521  
       2018-04-26 21:12:35 +08:00
    interface ?
    suyuanhxx
        20
    suyuanhxx  
    OP
       2018-04-26 22:41:28 +08:00
    @abmin521 #19 interface 砸咋了?文档有 bug ?...
    muzi
        21
    muzi  
       2018-04-28 18:23:27 +08:00
    mark 一下
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2858 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 42ms · UTC 09:23 · PVG 17:23 · LAX 01:23 · JFK 04:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.