GoLang 基本语法介绍

2019-04-25 golang

简单介绍常见的语法,例如 import、异常处理、反射等。

介绍

GoLang 是静态变量类型,可以自动推断类型,但是一旦定义就不能改变变量的类型,通常,可以通过 = 进行赋值,而 := 则会同时声明变量并赋值。

//----- 使用 = 必须先用 var 声明
var a
a = 100
var b = 100
var c int = 100

//----- 使用 := 系统会自动推断类型,不需要var关键字
d := 100

类型简介

GoLang 有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有切片、结构体、函数、map、channel等,其中基本类型包括了:

  • 整形 int/uint 与平台相关,可能是 32bits 或者 64bits,也可以指定具体长度,例如 int32uint64 等。
  • 布尔 bool 只能是 true 或者 false,不能赋值为 01

类型别名

可以通过 type 关键字把一个类型转换成另外一个类型,有两种方式:A) 类型定义,新定义了一种类型;B) 类型别名,只是将源类型重新起了个名字。如下是定义方式:

//----- 类型定义
type Student struct {
    name String
    age int
}
type Age int
type Height int
type Grade int

//----- 类型别名
type Stu = Student
type Age = int

类型定义不同于别名,关键是定义了一系列互不相干的行为特征:通过这些互不相干的行为特征,本质上同一的事物表现出不同事物的特征,整数还是整数,但年龄却不是高度也不是分数。

可以分别为 Age、Height、Grade 定义出下列不同的行为(表示为方法或者函数):

// 超过50岁算老年
func (a Age) Old() bool {
    return a > 50
}
// 高于120cm需要买票
func (l Height) NeedTicket() bool {
    return l > 120
}
// 60分及格
func (g Grade) Pass() bool {
    return g >= 60
}

类型转换

用来在不同但 相互兼容 的类型之间的相互转换的方式。

package main

import "fmt"

func main() {
    var v1 int = 7
    v2 := int64(v1)
    v3 := float32(v1)
    //v4 := []int8(v1)

    fmt.Printf("%T->%v\n", v1, v1)
    fmt.Printf("%T->%v\n", v2, v2)
    fmt.Printf("%T->%v\n", v3, v3)

    v := new(int32)
    fmt.Printf("%T->%v\n", v, v)
    vv := (*int32)(v)
    fmt.Printf("%T->%v\n", vv, vv)
}

其中 v4 的类型不兼容,所以会直接报错。另外,对于 (*int32)(v) 操作,不能修改为 *int32(v),因为后者等价于 *(int32(v)) ,显然与预期不符。

类型断言

其实与类型转换相同,只是断言用在接口中,也可以查看 Golang 语法之接口 中关于类型的判断。

package main

import "fmt"

func main() {
    //var data interface{} = "99"
    var data interface{} = 99

    if v, ok := data.(int); !ok {
        fmt.Printf("invlid type\n")
    } else {
        fmt.Printf("%T->%d\n", v, v)
    }
}

常用类型

数组 VS. 切片

数组是内置类型,相同数据类型的集合,下标从 0 开始,初始化后长度固定,且无法修改其长度。当作为方法的入参传入时将复制一份数组而不是引用同一指针,而且长度也是其类型的一部分,可以通过内置函数 len(array) 获取其长度。

可以通过如下方式初始化。

// 长度为5的数组,其元素分别为1, 2, 3, 4, 5
[5] int {1,2,3,4,5}

// 长度为5的数组,未赋值的默认是 0 ,也就是其元素值依次为1, 2, 0, 0, 0
[5] int {1,2}

// 长度为5的数组,其长度是根据初始化时指定的元素个数决定的
[...] int {1,2,3,4,5}

// 长度为5的数组,key:value,其元素值依次为:0,0,1,2,3。在初始化时指定了2,3,4索引中对应的值:1,2,3
[5] int { 2:1,3:2,4:3}

// 长度为5的数组,起元素值依次为:0,0,1,0,3。由于指定了最大索引4对应的值3,根据初始化的元素个数确定其长度为5
[...] int {2:1,4:3}

数组遍历

通常有如下的两种遍历方式。

package main

import "fmt"

func main() {
    var arr = [...]int{1, 2, 3, 4, 5}

    for idx, val := range arr {
        fmt.Printf("array[%d] = %d\n", idx, val)
    }
    fmt.Println(">>>>>>>>>>>>>")

    for idx := 0; idx < len(arr); idx++ {
        fmt.Printf("array[%d] = %d\n", idx, arr[idx])
    }
}

函数

在 golang 中,支持匿名函数和闭包,其中定义函数的方式如下:

func (p myType) funcName ( a, b int, c string ) ( r, s int ) {
    return
}

包括了定义函数的关键字 func,函数名称 funcName,入参 a, b int, c string,返回值 r,s int 以及函数体 {}

而且,golang 可以为某个类型定义函数,也即为类型对象定义方法,也就是 p myType 参数,当然这不是必须的,如果为空则纯粹是一个函数,可以通过包名称访问。

switch

类似于其它语言的 switch 语句,但同时继承了 GoLang 的简单有效,导致其语法略有区别。

package main

import "fmt"

func main() {
    i := 3

    switch i {
    case 0:
        fmt.Println("0")
    case 1, 2, 3:
        fmt.Println("1, 2, 3")
    case 4:
        fallthrough
    case 5:
        fmt.Println("4 or 5")
    default:
        fmt.Println("Default")
    }

    switch {
    case i <= 9:
        fmt.Println("range(, 9]")
    case i > 9 && i < 15:
        fmt.Println("range(9, 15)")
    }
}

在每个 case 语句之后,默认会带有一个 break 语句,在匹配成功后不会自动向下执行其它分支,而是跳出整个 switch 语句,不过可以通过 fallthrough 语句强制执行后面的分支代码。

如上代码中,当 i := 4 或者 i := 5 时,两者的输出相同。

另外,也可以不在 switch 语句后添加变量,那么此时的行为与 if ... else ... 相同,也就是如上的最后示例。

struct

通过结构体新建对象时的语法比较多,而且相比而言有些特殊。

type Poem struct {
    Title  string
    Author string
    intro  string
}

需要注意下访问权限,如果属性的开头字母是大写的则在其它包中可以被访问,否则只能在本包中访问;类的声明和方法亦是如此。然后,可以通过如下的方法进行赋值。

var poem1 *Poem
poem1 = &Poem{}
poem1.Author = "Heine"

poem2 := &Poem{Author: "Heine"}
poem3 := new(Poem)
poem3.Author = "Heine"
poem4 := Poem{}
poem4.Author = "Heine"
poem5 := Poem{Author: "Heine"}

嵌入类型

结构体类型可以包含匿名或者嵌入字段,该类型的名字会充当嵌入字段的字段名。

package main

import (
    "log"
)

type User struct {
    Name  string
    Email string
}

type Admin struct {
    User
    Level string
}

func (u *User) Notify() error {
    log.Printf("User: Sending User Email To %s<%s>\n", u.Name, u.Email)
    return nil
}

func (a *Admin) Notify() error {
    log.Printf("Admin: Sending Admin Email To %s<%s>\n", a.Name, a.Email)
    return nil
}

type Notifier interface {
    Notify() error
}

func SendNotification(notify Notifier) error {
    return notify.Notify()
}

func main() {
    admin := &Admin{
        User: User{
            Name:  "AriesDevil",
            Email: "ariesdevil@xxoo.com",
        },
        Level: "super",
    }

    SendNotification(admin)
    admin.Notify()
    admin.User.Notify()
}

如果对于子类重新定义了接口,那么默认调用的时候是子类的,也可以显式调用父类。

格式化

当定义了一个结构体之后,如果要格式化输出内容,可以对改类型定义一个 String() 方法,这样像 fmt.Println() fmt.Printf() 使用 %v 参数,就会直接调用该函数。

package main

import (
    "fmt"
)

type Poem struct {
    Title  string
    Author string
    intro  string
}

func (p *Poem) String() string {
    return fmt.Sprintf("<%s> %s", p.Title, p.Author)
}

func main() {
    p := &Poem{
        Title:  "Harry Potter",
        Author: "J. K. Rowling",
    }
    fmt.Printf("%T\n", p)
    fmt.Println(p)
    fmt.Printf("%v\n", p)
    fmt.Printf("%#v\n", p)
}

最终输出的内容为。

*main.Poem
<Harry Potter> J. K. Rowling
<Harry Potter> J. K. Rowling
&main.Poem{Title:"Harry Potter", Author:"J. K. Rowling", intro:""}

注意,不要在 String() 函数中涉及上述相关调用,如 %v 格式化,会导致无限递归调用。

MAP

在 1.9 版本的 sync 库中引入了并发 map 数据结构。

The new Map type in the sync package is a concurrent map with
amortized-constant-time loads, stores, and deletes. It is safe
for multiple goroutines to call a Map's methods concurrently.

GoLang 内置类型 MAP 是用哈希表实现的,可以通过 map[KeyType]ValueType 定义,ValueType 可以是任意类型,包括 map,而 KeyType 需要是可比较的类型,包括 == != < <= > >=

如果请求的 Key 不存在,则返回 Value 类型的零值,其中 int 为 0、string""

示例

package main

import "fmt"

func main() {
    /*
        var m1 map[string]string = map[string]string{}
        var m2 map[string]string
        m2 = make(map[string]string) // OR
    */

    cities := map[string]string{
        "ZheJiang": "HangZou",
    }

    newCities := cities
    newCities["ZheJiang"] = "HangZhou" // Also change cities

    cities["LiaoNing"] = "ShenYang" // len(cities) == 1

    for c := range cities {
        fmt.Println("A: Capital of", c, "is", cities[c])
    }

    for c, v := range cities {
        fmt.Println("B: Capital of", c, "is", v)
    }

    if c, ok := cities["ShanDong"]; ok { // OR JUST CHECK _, ok := cities["ShanDong"]
        fmt.Println("Capital of LiaoNing is", c)
    } else {
        fmt.Println("Get capital failed")
    }

    fmt.Println(cities["NotExists"] == "")
    delete(cities, "LiaoNing")
    delete(cities, "NotExists") // No side effect

    fmt.Println("Current length", len(cities))
}

稳定性

当使用 range 循环遍历 MAP 时,遍历的顺序是不确定的,并且不能保证遍历顺序和下次遍历的顺序相同。

在运行时对 MAP 遍历的顺序做随机化处理,如果依赖于遍历顺序稳定性,必须自己去维护一个遍历顺序的独立数据结构,例如切片。

其它

与切片一样,MAP 是引用类型,当一个 MAP 赋值给一个新的变量时,它们都指向同一个内部数据结构,改变其中一个也会反映到另一个。

MAP 不能通过 == 操作符比较是否相等,能用来检测 MAP 是否为 nil

字符串操作

这应该是最常见的,在 Golang 中有多种方式可以完成拼接,详见如下的测试程序。

package main

import (
    "bytes"
    "fmt"
    "strings"
    "time"
)

var ways = []string{
    "fmt.Sprintf ",
    "+           ",
    "strings.Join",
    "bytes.Buffer",
}

func benchmarkStringFunction(n int, idx int) {
    var s string
    var buf bytes.Buffer

    v := "hello world, just for test"
    begin := time.Now()
    for i := 0; i < n; i++ {
        switch idx {
        case 0: // fmt.Sprintf
            s = fmt.Sprintf("%s[%s]", s, v)
        case 1: // string +
            s = s + "[" + v + "]"
        case 2: // strings.Join
            s = strings.Join([]string{s, "[", v, "]"}, "")
        case 3: // stable bytes.Buffer
            buf.WriteString("[")
            buf.WriteString(v)
            buf.WriteString("]")
        }

    }
    if idx == 3 {
        s = buf.String()
    }
    fmt.Printf("string len: %d\t", len(s))
    fmt.Printf("time of [%s]=\t %v\n", ways[idx], time.Since(begin))
}

func main() {
    for idx, _ := range ways {
        benchmarkStringFunction(10000, idx)
    }
}

执行结果如下。

string len: 280000      time of [fmt.Sprintf ]=  366.809538ms
string len: 280000      time of [+           ]=  231.356836ms
string len: 280000      time of [strings.Join]=  497.997435ms
string len: 280000      time of [bytes.Buffer]=  867.259µs

结论: A) strings.Join 最慢;B) 其次为 fmt.Sprintfstring +;C) 最快为 bytes.Buffer

协程

另外,一种方式是一直阻塞在管道中,利用 for 循环遍历。

package main

import (
    "fmt"
    "time"
)

func readCommits(commitC <-chan *string) {
    for data := range commitC {
        if data == nil {
            fmt.Println("Got nil data")
            continue
        }
        fmt.Println(*data)
    }
}

func main() {
    commitC := make(chan *string)

    go func() {
        for {
            s := "hi"
            time.Sleep(1 * time.Second)

            commitC <- &s
        }
    }()

    readCommits(commitC)
}

反射

反射允许你可以在运行时检查类型,包括检查、修改、创建变量、函数、结构体等。

package main

import (
    "fmt"
    "reflect"
)

type Foobar struct {
    name string
}

func (f *Foobar) GetName() string {
    return f.name
}

func main() {
    s := "this is string"
    fmt.Println(reflect.TypeOf(s))
    fmt.Println(reflect.ValueOf(s))

    var x float64 = 3.4
    fmt.Println(reflect.ValueOf(x))

    a := new(Foobar)
    a.name = "foo bar"
    t := reflect.TypeOf(a)
    fmt.Println(t.NumMethod())
    b := reflect.ValueOf(a).MethodByName("GetName").Call([]reflect.Value{})
    fmt.Println(b[0])
}

竞态条件

如下是一个简单的示例,简单来说,是对一个内存中的值进行累加,

package main

import (
    "fmt"
    "sync"
)

var (
    N         = 0
    waitgroup sync.WaitGroup
)

func counter(number *int) {
    *number++
    waitgroup.Done()
}

func main() {
    for i := 0; i < 1000; i++ {
        waitgroup.Add(1)
        go counter(&N)
    }
    waitgroup.Wait()
    fmt.Println(N)
}

如果运行多次,可以发现绝大多数情况下其累加值不是 1000 。

解决方法是在执行累加时,对操作加锁。

package main

import (
    "fmt"
    "sync"
)

var (
    N         = 0
    mutex     sync.Mutex
    waitgroup sync.WaitGroup
)

func counter(number *int) {
    mutex.Lock()
    *number++
    mutex.Unlock()
    waitgroup.Done()
}

func main() {
    for i := 0; i < 1000; i++ {
        waitgroup.Add(1)
        go counter(&N)
    }
    waitgroup.Wait()
    fmt.Println(N)
}

竞态条件检测

GoLang 工具内置了竞态检测工具,只需要加上 -race 即可。

$ go test -race mypkg    // test the package
$ go run -race mysrc.go  // compile and run the program
$ go build -race mycmd   // build the command
$ go install -race mypkg // install the package

使用该参数,GO 会记录不同线程对共享变量的访问情况,如果发现非同步的访问则会退出并打印告警信息。

枚举类型

GoLang 并没有提供 enum 的定义,不过可以使用 const 来模拟枚举类型。

const (
    STATUS_NORMAL int = 0
    STATUS_FAILED int = 1
)

另外,也可以使用 iota

iota

这是 GoLang 语言中的常量计数器,只能在常量的表达式中使用;该关键字出现时将被重置为 0,然后每新增一行常量声明将使 iota 计数一次。

例如,time 包中定义时间类型的方式如下,其中的 _ 符号表示跳过。

type Weekday int

const (
    Sunday Weekday = iota // 0
    Monday    // 1
    Tuesday   // 2
    Wednesday // 3
    _
    Friday    // 5
    Saturday  // 6
)

注意,iota 的增长是按照行顺序,如果在同一行,那么值是不会增长的。

其它常见场景。

位掩码

package main

import (
    "fmt"
)

type Flags uint

const (
    FlagUp           Flags = 1 << iota // is up
    FlagBroadcast                      // supports broadcast access capability
    FlagLoopback                       // is a loopback interface
    FlagPointToPoint                   // belongs to a point-to-point link
    FlagMulticast                      // supports multicast access capability
)

func IsUp(v Flags) bool     { return v&FlagUp == FlagUp }
func TurnDown(v *Flags)     { *v &^= FlagUp }
func SetBroadcast(v *Flags) { *v |= FlagBroadcast }
func IsCast(v Flags) bool   { return v&(FlagBroadcast|FlagMulticast) != 0 }

func main() {
    var v Flags = FlagMulticast | FlagUp
    fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true"
    TurnDown(&v)
    fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false"
    SetBroadcast(&v)
    fmt.Printf("%b %t\n", v, IsUp(v))   // "10010 false"
    fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true"
}

定义数量级

type ByteSize float64

const (
    _           = iota                   // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)       // 1 << (10*1)
    MB                                   // 1 << (10*2)
    GB                                   // 1 << (10*3)
    TB                                   // 1 << (10*4)
    PB                                   // 1 << (10*5)
    EB                                   // 1 << (10*6)
    ZB                                   // 1 << (10*7)
    YB                                   // 1 << (10*8)
)

它并不能用于产生 1000 的幂,因为 GoLang 并没有计算幂的运算符。

中间间隔

const (
    i = iota     // 0
    j = 3.14     // 3.14
    k = iota     // 2
    l            // 3
)

语法糖

主要有两种用法:A) 用于函数有多个不定参数场景;B) slice 可以被打散进行传递。

package main

import "fmt"

func foobar(args ...string) {
    fmt.Printf("argc %d\n", len(args))
    for _, v := range args {
        fmt.Println(v)
    }
}

func main() {
    var foostr = []string{"hello", "world"}
    var barstr = []string{"hi", "fooo", "baar,"}

    foobar("foo", "bar", "foobar")

    fmt.Println(append(barstr, foostr...))
}

Byte VS. Rune

两种实际上是 uint8uint32 类型,byte 用来强调数据是 RawData,而不是数字;而 rune 用来表示 Unicode 编码的 CodePoint。

中文字符使用 3 个字节保存(为什么使用的是3个字节保存,还没有搞清楚)。

s := "hello你好"
fmt.Println(len(s))         // 11
fmt.Println(len([]rune(s))) // 7,需要先转换为rune的切片在使用内置len函数

GoLang 中 string 底层是通过 byte 数组实现,中文字符在 Unicode 下占 2 个字节,而在 UTF-8 编码中占 3 个字节,而 GoLang 默认编码就是 UTF-8 。

s = "你好"
fmt.Println(len(s))         // 6
fmt.Println(len([]rune(s))) // 2
s = "你"
fmt.Println([]byte(s)) // 三个字节,也就是中文的表示方法
fmt.Println(rune('你')) // 输出20320(0x4F60),'你' 的编码