Go语言学习笔记

Go语言学习笔记

Go语言初识

  • Go语言简介

Go 是一门被设计用来构建简单、高效、可信赖软件的开源程序设计语言。

Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。

Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。

  • Go 语言特色

    • 简洁、快速、安全

    • 并行、有趣、开源

    • 内存管理、数组安全、编译迅速

  • Go语言用途

    Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。

    对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。

GO语言实践

  • hello world程序
package main

import "fmt"

func main() {
   /* 这是我的第一个简单的程序 */
   fmt.Println("Hello, World!")
}
  • Go 拥有多种值类型,包括字符串、整型、浮点型、布尔型等。

    package main
    
    import "fmt"
    
    func main() {
    
        fmt.Println("go" + "lang") //字符串可以通过 + 连接。
    
        fmt.Println("1+1 =", 1+1)
        fmt.Println("7.0/3.0 =", 7.0/3.0)
    
        fmt.Println(true && false)
        fmt.Println(true || false)
        fmt.Println(!true)
    }
    
  • 变量

​ 在 Go 中,变量 需要显式声明,并且在函数调用等情况下, 编译器会检查其类型的正确性。

​ var可以声明 1 个或者多个变量。Go 会自动推断已经有初始值的变量的类型。声明后却没有给出对应的初始值时,变量将会初始化为 零值 。 例如,int 的零值是 0:= 语法是声明并初始化变量的简写, 例如 var f string = "short" 可以简写为右边这样,也比较常用的写法,不能用于进行全局变量声明。

package main

import "fmt"

func main() {

    var a = "initial"
    fmt.Println(a)

    var b, c int = 1, 2
    fmt.Println(b, c)

    var d = true
    fmt.Println(d)

    var e int
    fmt.Println(e)

    f := "short"
    fmt.Println(f)
}
  • 常量

    Go 支持字符、字符串、布尔和数值 常量

    const 用于声明一个常量。

    const 语句可以出现在任何 var 语句可以出现的地方

    常数表达式可以执行任意精度的运算

    数值型常量没有确定的类型,直到被给定某个类型,比如显式类型转化。

    一个数字可以根据上下文的需要(比如变量赋值、函数调用)自动确定类型。 举个例子,这里的 math.Sin 函数需要一个 float64 的参数,n 会自动确定类型。

    package main
    
    import (
        "fmt"
        "math"
    )
    
    const s string = "constant"
    
    func main() {
        fmt.Println(s)
    
        const n = 500000000
    
        const d = 3e20 / n
        fmt.Println(d)
    
        fmt.Println(int64(d))
    
        fmt.Println(math.Sin(n))
    }
    
  • for循环

    for 是 Go 中唯一的循环结构。这里会展示 for 循环的三种基本使用方式。

    package main
    
    import "fmt"
    
    func main() {
    
        i := 1
        for i <= 3 {
            fmt.Println(i)
            i = i + 1
        }
    
        for j := 7; j <= 9; j++ {
            fmt.Println(j)
        }
    
        for {
            fmt.Println("loop")
            break
        }
    
        for n := 0; n <= 5; n++ {
            if n%2 == 0 {
                continue
            }
            fmt.Println(n)
        }
    }
    
  • if/else分支

    ifelse 分支结构在 Go 中非常直接。

    你可以不要 else 只用 if 语句。

    在条件语句之前可以有一个声明语句;在这里声明的变量可以在这个语句所有的条件分支中使用。

    注意,在 Go 中,条件语句的圆括号不是必需的,但是花括号是必需的。

    Go 没有三目运算符, 即使是基本的条件判断,依然需要使用完整的 if 语句。

    package main
    
    import "fmt"
    
    func main() {
    
        if 7%2 == 0 {
            fmt.Println("7 is even")
        } else {
            fmt.Println("7 is odd")
        }
    
        if 8%4 == 0 {
            fmt.Println("8 is divisible by 4")
        }
    
        if num := 9; num < 0 {
            fmt.Println(num, "is negative")
        } else if num < 10 {
            fmt.Println(num, "has 1 digit")
        } else {
            fmt.Println(num, "has multiple digits")
        }
    }
    
  • swith分支结构

    switch 是多分支情况时快捷的条件语句。

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
    
        i := 2
        fmt.Print("write ", i, " as ")
        switch i {
        case 1:
            fmt.Println("one")
        case 2:
            fmt.Println("two")
        case 3:
            fmt.Println("three")
        }
    
        switch time.Now().Weekday() {
        case time.Saturday, time.Sunday:
            fmt.Println("It's the weekend")
        default:
            fmt.Println("It's a weekday")
        }
    /*不带表达式的 switch 是实现 if/else 逻辑的另一种方式。 
    这里还展示了 case 表达式也可以不使用常量。*/
        t := time.Now()
        switch {
        case t.Hour() < 12:
            fmt.Println("It's before noon")
        default:
            fmt.Println("It's after noon")
        }
    /*类型开关 (type switch) 比较类型而非值。可以用来发现一个接口值的类型。 
    在这个例子中,变量 t 在每个分支中会有相应的类型。*/
        whatAmI := func(i interface{}) {
            switch t := i.(type) {
            case bool:
                fmt.Println("I'm a bool")
            case int:
                fmt.Println("I'm an int")
            default:
                fmt.Printf("Don't know type %T\n", t)
            }
        }
        whatAmI(true)
        whatAmI(1)
        whatAmI("hey")
    }
    
  • 数组

    在 Go 中,数组 是一个具有编号且长度固定的元素序列。

    可以使用 array[index] = value 语法来设置数组指定位置的值, 或者用 array[index] 得到值。

    内置函数 len 可以返回数组的长度。

    package main
    
    import "fmt"
    
    func main() {
    /*我们创建了一个刚好可以存放 5 个 int 元素的数组 a。
    元素的类型和长度都是数组类型的一部分。 数组默认值是零值,
    对于 int 数组来说,元素的零值是 0。*/
        var a [5]int
        fmt.Println("emp:", a)
    
        a[4] = 100
        fmt.Println("set:", a)
        fmt.Println("get:", a[4])
    
        fmt.Println("len:", len(a))
    
        b := [5]int{1, 2, 3, 4, 5}
        fmt.Println("dcl:", b)
    
        var twoD [2][3]int
        for i := 0; i < 2; i++ {
            for j := 0; j < 3; j++ {
                twoD[i][j] = i + j
            }
        }
        fmt.Println("2d: ", twoD)
    }
    
  • 切片

    在 Go 程序中,相较于数组,用得更多的是 切片(slice)。我们接着来看切片。

    Slice 是 Go 中一个重要的数据类型,它提供了比数组更强大的序列交互方式。

    与数组不同,slice 的类型仅由它所包含的元素的类型决定(与元素个数无关)。 要创建一个长度不为 0 的空 slice,需要使用内建函数 make

    可以和数组一样设置和得到值

    除了基本操作外,slice 支持比数组更丰富的操作。比如 slice 支持内建函数 append, 该函数会返回一个包含了一个或者多个新值的 slice。 注意由于 append 可能返回一个新的 slice,我们需要接收其返回值。

    深拷贝:拷贝的是数据本身。值类型的数据默认都是深拷贝:
    arry,int,float,string,struct,bool

    slice 还可以 copy。这里我们创建一个空的和 s 有相同长度的 slice——c, 然后将 s 复制给 c

    浅拷贝:拷贝的是数据 地址。导致多个变量指向同一个内存
    引用类型的数据,默认都是浅拷贝:slice,map
    切片是引用类型数据,直接拷贝的地址

    slice 支持通过 slice[low:high] 语法进行“切片”操作。

    Slice 可以组成多维数据结构。内部的 slice 长度可以不一致,这一点和多维数组不同。

    package main
    
    import "fmt"
    
    func main() {
    //我们创建了一个长度为 3 的 string 类型的 slice(初始值为零值)。
        s := make([]string, 3)
        fmt.Println("emp:", s)
    
        s[0] = "a"
        s[1] = "b"
        s[2] = "c"
        fmt.Println("set:", s)
        fmt.Println("get:", s[2])
    
        fmt.Println("len:", len(s))
    
        s = append(s, "d")
        s = append(s, "e", "f")
        fmt.Println("apd:", s)
    
        c := make([]string, len(s))
        copy(c, s)
        fmt.Println("cpy:", c)
    
        l := s[2:5] //可以得到一个包含元素 s[2]、s[3] 和 s[4] 的 slice。
        fmt.Println("sl1:", l)
    
        l = s[:5] //这个 slice 包含从 s[0] 到 s[5](不包含 5)的元素。
        fmt.Println("sl2:", l)
    
        l = s[2:]//这个 slice 包含从 s[2](包含 2)之后的元素。
        fmt.Println("sl3:", l)
    
        t := []string{"g", "h", "i"}
        fmt.Println("dcl:", t)
    
        twoD := make([][]int, 3)
        for i := 0; i < 3; i++ {
            innerLen := i + 1
            twoD[i] = make([]int, innerLen)
            for j := 0; j < innerLen; j++ {
                twoD[i][j] = i + j
            }
        }
        fmt.Println("2d: ", twoD)
    }
    
  • Map

    map 是 Go 内建的关联数据类型 (在一些其他的语言中也被称为 哈希(hash) 或者 字典(dict) )。

​ map:映射,是一种专门用来存储键值对的集合,属于引用类型
​ 存储特点:A:存储的是无序的键值对
​ B:键不能重复,并且与value值一一对应
​ map中的key不能重复,如果重复,那么新的value值会覆盖原来的,不会报错

要创建一个空 map,需要使用内建函数 makemake(map[key-type]val-type)

使用典型的 name[key] = val 语法来设置键值对。

使用 name[key] 来获取一个键的值。

内建函数 len 可以返回一个 map 的键值对数量。

内建函数 delete 可以从一个 map 中移除键值对。

当从一个 map 中取值时,还有可以选择是否接收的第二个返回值,该值表明了 map 中是否存在这个键。 这可以用来消除 键不存在键的值为零值 产生的歧义, 例如 0""。这里我们不需要值,所以用 空白标识符(blank identifier) _ 将其忽略。

package main

import "fmt"

func main() {

    m := make(map[string]int)

    m["k1"] = 7
    m["k2"] = 13

    fmt.Println("map:", m)

    v1 := m["k1"]
    fmt.Println("v1: ", v1)

    fmt.Println("len:", len(m))

    delete(m, "k2")
    fmt.Println("map:", m)

    _, prs := m["k2"]
    fmt.Println("prs:", prs)

    n := map[string]int{"foo": 1, "bar": 2}
    fmt.Println("map:", n)
}
  • range遍历

    range 用于迭代各种各样的数据结构。 让我们来看看如何在我们已经学过的数据结构上使用 range

    range 在数组和 slice 中提供对每项的索引和值的访问。 上面我们不需要索引,所以我们使用 空白标识符 _ 将其忽略。 实际上,我们有时候是需要这个索引的。

    range 在字符串中迭代 unicode 码点(code point)。 第一个返回值是字符的起始字节位置,然后第二个是字符本身。

    package main
    
    import "fmt"
    
    func main() {
    
        nums := []int{2, 3, 4}
        sum := 0
        for _, num := range nums {
            sum += num
        }
        fmt.Println("sum:", sum)
    
        for i, num := range nums {
            if num == 3 {
                fmt.Println("index:", i)
            }
        }
    
        kvs := map[string]string{"a": "apple", "b": "banana"}
        for k, v := range kvs {
            fmt.Printf("%s -> %s\n", k, v)
        }
    
        for k := range kvs {
            fmt.Println("key:", k)
        }
    /*range在字符串迭代Unicode,第一个值是字符的起始字节位置(ASCII码),依次是下个字符的ASCII码*/
        for i, c := range "go" {
            fmt.Println(i, c)
        }
    }
    
  • 函数 是 Go 的核心。我们将通过一些不同的例子来进行学习它。

    Go 需要明确的 return,也就是说,它不会自动 return 最后一个表达式的值

    当多个连续的参数为同样类型时, 可以仅声明最后一个参数的类型,忽略之前相同类型参数的类型声明。

    Go 函数还有很多其他的特性。 其中一个就是多值返回。

    package main
    
    import "fmt"
    
    func plus(a int, b int) int {
    
        return a + b
    }
    
    func plusPlus(a, b, c int) int {
        return a + b + c
    }
    
    func main() {
    
        res := plus(1, 2)
        fmt.Println("1+2 =", res)
    
        res = plusPlus(1, 2, 3)
        fmt.Println("1+2+3 =", res)
    }
    
  • 多返回值

    Go 原生支持 多返回值。 这个特性在 Go 语言中经常用到,例如用来同时返回一个函数的结果和错误信息。

    package main
    
    import "fmt"
    
    func vals() (int, int) {
        return 3, 7
    }
    
    func main() {
    
        a, b := vals()
        fmt.Println(a)
        fmt.Println(b)
    
        _, c := vals()
        fmt.Println(c)
    }
    
  • 变参函数

    可变参数函数。 在调用时可以传递任意数量的参数。 例如,fmt.Println 就是一个常见的变参函数。

    package main
    
    import "fmt"
    \\ 函数接受任意数量的 int 作为参数。
    func sum(nums ...int) {
        fmt.Print(nums, " ")
        total := 0
        for _, num := range nums {
            total += num
        }
        fmt.Println(total)
    }
    
    func main() {
    
        sum(1, 2)
        sum(1, 2, 3)
    \\如果你有一个含有多个值的 slice,想把它们作为参数使用, 你需要这样调用 func(slice...)。
        nums := []int{1, 2, 3, 4}
        sum(nums...)
    }
    
  • 闭包

    Go 支持匿名函数, 并能用其构造 闭包。 匿名函数在你想定义一个不需要命名的内联函数时是很实用的。

    intSeq 函数返回一个在其函数体内定义的匿名函数。 返回的函数使用闭包的方式 隐藏 变量 i。 返回的函数 隐藏 变量 i 以形成闭包。

    调用 intSeq 函数,将返回值(一个函数)赋给 nextInt。 这个函数的值包含了自己的值 i,这样在每次调用 nextInt 时,都会更新 i 的值。

    闭包=外部函数的变量+内部函数调用变量

    package main
    
    import "fmt"
    
    func intSeq() func() int {
        i := 0
        return func() int { //匿名函数
            i++
            return i  //闭包
        }
    }
    
    func main() {
    
        nextInt := intSeq()
    
        fmt.Println(nextInt())
        fmt.Println(nextInt())
        fmt.Println(nextInt())
    
        newInts := intSeq()
        fmt.Println(newInts())
    }
    
  • 递归

    Go 支持 递归

    package main
    
    import "fmt"
    
    func fact(n int) int {
        if n == 0 {
            return 1
        }
        return n * fact(n-1)
    }
    
    func main() {
        fmt.Println(fact(7))
    }
    
  • 指针

    Go 支持 指针, 允许在程序中通过 引用传递 来传递值和数据结构。

    package main
    
    import "fmt"
    /*通过两个函数:zeroval 和 zeroptr 来比较 指针 和 值。 zeroval 有一个 int 型参数,所以使用值传递。 zeroval 将从调用它的那个函数中得到一个实参的拷贝:ival。*/
    func zeroval(ival int) {
        ival = 0
    }
    /*zeroptr 有一个和上面不同的参数:*int,这意味着它使用了 int 指针。 紧接着,函数体内的 *iptr 会 解引用 这个指针,从它的内存地址得到这个地址当前对应的值。 对解引用的指针赋值,会改变这个指针引用的真实地址的值。*/
    func zeroptr(iptr *int) {
        *iptr = 0
    }
    
    func main() {
        i := 1
        fmt.Println("initial:", i)
    /*zeroval 在 main 函数中不能改变 i 的值, 但是 zeroptr 可以,因为它有这个变量的内存地址的引用。*/
        zeroval(i)
        fmt.Println("zeroval:", i)
    
        zeroptr(&i)
        fmt.Println("zeroptr:", i)
    
        fmt.Println("pointer:", &i)
    }
    //输出结果:
    initial: 1
    zeroval: 1
    zeroptr: 0
    pointer: 0x42131100
    
  • 结构体

    Go 的结构体(struct) 是带类型的字段(fields)集合。 这在组织数据时非常有用。

    可以在初始化一个结构体元素时指定字段名字。

    省略的字段将被初始化为零值。

    & 前缀生成一个结构体指针。

    使用.来访问结构体字段。

    也可以对结构体指针使用. - 指针会被自动解引用。

    结构体是可变(mutable)的。

    package main
    
    import "fmt"
    
    type person struct {
        name string
        age  int
    }
    
    func main() {
    
        fmt.Println(person{"Bob", 20})
    
        fmt.Println(person{name: "Alice", age: 30})
    
        fmt.Println(person{name: "Fred"})
        
        fmt.Println(&person{"tom",25})
    
        fmt.Println(&person{name: "Ann", age: 40})
    
        s := person{name: "Sean", age: 50}
        fmt.Println(s.name)
    
        sp := &s
        fmt.Println(sp.age)
    
        sp.age = 51
        fmt.Println(sp.age)
    }
    
  • 方法

    Go 支持为结构体类型定义方法(methods)

    package main
    
    import "fmt"
    
    type rect struct {
        width, height int
    }
    
    func (r *rect) area() int {  //area 是一个拥有 *rect 类型接收器(receiver)的方法。
        return r.width * r.height
    }
    
    func (r rect) perim() int { //可以为值类型或者指针类型的接收者定义方法。 这是一个值类型接收者的例子。
        return 2*r.width + 2*r.height
    }
    
    func main() {
        r := rect{width: 10, height: 5}
    
        fmt.Println("area: ", r.area())
        fmt.Println("perim:", r.perim())
    /*调用方法时,Go 会自动处理值和指针之间的转换。 想要避免在调用方法时产生一个拷贝,或者想让方法可以修改接受结构体的值, 你都可以使用指针来调用方法。*/
        rp := &r 
        fmt.Println("area: ", rp.area())
        fmt.Println("perim:", rp.perim())
    }
    
  • 接口

    方法签名的集合叫做:接口(Interfaces)

    package main
    
    import (
        "fmt"
        "math"
    )
    
    type geometry interface {
        area() float64
        perim() float64
    }
    
    type rect struct {
        width, height float64
    }
    type circle struct {
        radius float64
    }
    /*在 Go 中实现一个接口,我们只需要实现接口中的所有方法。 这里我们为 rect 实现了 geometry 接口。*/
    func (r rect) area() float64 {
        return r.width * r.height
    }
    func (r rect) perim() float64 {
        return 2*r.width + 2*r.height
    }
    
    func (c circle) area() float64 {
        return math.Pi * c.radius * c.radius
    }
    func (c circle) perim() float64 {
        return 2 * math.Pi * c.radius
    }
    /*如果一个变量实现了某个接口,我们就可以调用指定接口中的方法。 这儿有一个通用的 measure 函数,我们可以通过它来使用所有的 geometry。*/
    func measure(g geometry) {
        fmt.Println(g)
        fmt.Println(g.area())
        fmt.Println(g.perim())
    }
    
    func main() {
        r := rect{width: 3, height: 4}
        c := circle{radius: 5}
    /*结构体类型 circle 和 rect 都实现了 geometry 接口, 所以我们可以将其实例作为 measure 的参数。*/
        measure(r)
        measure(c)
    }
    
  • 错误处理

    符合 Go 语言习惯的做法是使用一个独立、明确的返回值来传递错误信息。 这与 Java、Ruby 使用的异常(exception) 以及在 C 语言中有时用到的重载 (overloaded) 的单返回/错误值有着明显的不同。 Go 语言的处理方式能清楚的知道哪个函数返回了错误,并使用跟其他(无异常处理的)语言类似的方式来处理错误。

    错误通常是最后一个返回值并且是 error 类型,它是一个内建的接口。

    package main
    
    import (
        "errors"
        "fmt"
    )
    
    func f1(arg int) (int, error) {
        if arg == 42 {
    
            return -1, errors.New("can't work with 42") //errors.New 使用给定的错误信息构造一个基本的 error 值。
    
        }
    
        return arg + 3, nil //返回错误值为 nil 代表没有错误。
    }
    /*可以通过实现 Error() 方法来自定义 error 类型。 这里使用自定义错误类型来表示上面例子中的参数错误。
    使用 &argError 语法来建立一个新的结构体, 并提供了 arg 和 prob 两个字段的值。*/
    
    type argError struct {
        arg  int
        prob string
    }
    
    func (e *argError) Error() string {
        return fmt.Sprintf("%d - %s", e.arg, e.prob)
    }
    
    func f2(arg int) (int, error) {
        if arg == 42 {
    
            return -1, &argError{arg, "can't work with it"}
        }
        return arg + 3, nil
    }
    
    func main() {
    /* 注意,在 if 的同一行进行错误检查,是 Go 代码中的一种常见用法。*/
        for _, i := range []int{7, 42} {
            if r, e := f1(i); e != nil {
                fmt.Println("f1 failed:", e)
            } else {
                fmt.Println("f1 worked:", r)
            }
        }
        for _, i := range []int{7, 42} {
            if r, e := f2(i); e != nil {
                fmt.Println("f2 failed:", e)
            } else {
                fmt.Println("f2 worked:", r)
            }
        }
    
        _, e := f2(42)
        if ae, ok := e.(*argError); ok {
            fmt.Println(ae.arg)
            fmt.Println(ae.prob)
        }
    }
    

    你想在程序中使用自定义错误类型的数据, 你需要通过类型断言来得到这个自定义错误类型的实例。

  • 协程

    协程(goroutine) 是轻量级的执行线程。

    package main
    
    import (
        "fmt"
        "time"
    )
    /*有一个函数叫做 f(s)。 我们一般会这样 同步地 调用它*/
    
    func f(from string) {
        for i := 0; i < 3; i++ {
            fmt.Println(from, ":", i) //
        }
    }
    
    func main() {
    
        f("direct")
    
        go f("goroutine") //使用 go f(s) 在一个协程中调用这个函数。 这个新的 Go 协程将会 并发地 执行这个函数。
    
        go func(msg string) {//匿名函数启动一个协程。
            fmt.Println(msg)
        }("going")
    
        time.Sleep(time.Second)
        fmt.Println("done")
    }
    

    我们运行这个程序时,首先会看到阻塞式调用的输出,然后是两个协程的交替输出。 这种交替的情况表示 Go runtime 是以并发的方式运行协程的。

  • 通道

    通道(channels) 是连接多个协程的管道。 你可以从一个协程将值发送到通道,然后在另一个协程中接收。

    package main
    
    import "fmt"
    
    func main() {
    
        messages := make(chan string) //使用 make(chan val-type) 创建一个新的通道。 通道类型就是他们需要传递值的类型。
    /*使用 channel <- 语法 发送 一个新的值到通道中。 这里我们在一个新的协程中发送 "ping" 到上面创建的 messages 通道中。*/
        go func() { messages <- "ping" }()
    /*使用 <-channel 语法从通道中 接收 一个值。 这里我们会收到在上面发送的 "ping" 消息并将其打印出来。*/
        msg := <-messages
        /*通过通道, 成功的将消息 "ping" 从一个协程传送到了另一个协程中*/
        fmt.Println(msg)
    }
    

    默认发送和接收操作是阻塞的,直到发送方和接收方都就绪。 这个特性允许我们,不使用任何其它的同步操作, 就可以在程序结尾处等待消息 "ping"

  • 通道缓冲

    默认情况下,通道是 无缓冲 的,这意味着只有对应的接收(<- chan) 通道准备好接收时,才允许进行发送(chan <-)。 有缓冲通道 允许在没有对应接收者的情况下,缓存一定数量的值。

    package main
    
    import "fmt"
    
    func main() {
    //make 了一个字符串通道,最多允许缓存 2 个值
        messages := make(chan string, 2)
    
        messages <- "buffered"
        messages <- "channel"
    
        fmt.Println(<-messages)
        fmt.Println(<-messages)
    }
    

    由于此通道是有缓冲的, 因此我们可以将这些值发送到通道中,而无需并发的接收。

  • 通道同步

    可以使用通道来同步协程之间的执行状态。 这儿有一个例子,使用阻塞接收的方式,实现了等待另一个协程完成。 如果需要等待多个协程,WaitGroup 是一个更好的选择。

你可能感兴趣的:(学习,golang,开发语言)