Go 语言入门指南:基础语法和常用特性解析| 青训营

69 阅读17分钟

1.1 基础语法-变量

Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。声明变量的一般形式是使用 var 关键字:

第一种,指定变量类型,如果没有初始化,则变量默认为零值(变量没有做初始化时系统默认设置的值)。

package main
import "fmt"
func main() {
    // 声明一个变量并初始化
    var a = "RUNOOB"
    fmt.Println(a)
​
    // 没有初始化就为零值
    var b int
    fmt.Println(b)
​
    // bool 零值为 false
    var c bool
    fmt.Println(c)
}
/** 输出
RUNOOB
0
false
/

第二种,根据值自行判定变量类型。

package main
import "fmt"
func main() {
    var d = true
    fmt.Println(d)
}
// 输出:true

第三种,如果变量已经使用 var 声明过了,再使用 := 声明变量,就产生编译错误,格式:

package main
import "fmt"
func main() {
    f := "千羽" // var f string = "Runoob"
    fmt.Println(f)
}
// 输出:千羽

package main
​
import (
    "fmt"
    "math"
)
​
func main() {
​
    var a = "initial"var b, c int = 1, 2var d = truevar e float64
​
    f := float32(e)
​
    g := a + "foo"
    fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
    fmt.Println(g)                // initialappleconst s string = "constant"
    const h = 500000000
    const i = 3e20 / h
    fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}
// 输出
// initial 1 2 true 0 0
// initialfoo                                                      
// constant 500000000 6e+11 -0.28470407323754404 0.7591753930288755

1.2 基础语法-if else

Go 语言提供了以下几种条件判断语句:

语句描述
if 语句if 语句 由一个布尔表达式后紧跟一个或多个语句组成。
if...else 语句if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。
if 嵌套语句你可以在 ifelse if 语句中嵌入一个或多个 ifelse if 语句。
switch 语句switch 语句用于基于不同条件执行不同动作。
select 语句select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

注意:Go 没有三目运算符,所以不支持 ?: 形式的条件判断。

go语言里面的if else写法和C或者C++类似。不同点是if后面没有括号。如果你写括号的话,那么在保存的时候你的编辑器会自动把你去掉。第二个不同点是Golang里面的if,它必须后面接大括号,就是你不能像C或者C++ 一样,直接把if里面的语句同一行。

package main
​
import "fmt"
​
func main() {
    // if 后面没有括号
    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")
    }
}

1.3基础语法-循环

go语言里面的循环,在go里面没有while循环、do while循环,只有唯一的一种for循环。最简单的for循环就是在for后面什么都不写,代表一个死循环。循环途中你可以用break跳出。在循环里面,你可以用break或者continue来跳出或者继续循环,

Go 语言提供了以下几种类型循环处理语句:

循环类型描述
for 循环重复执行语句块
循环嵌套在 for 循环中嵌套一个或多个 for 循环

循环控制语句

循环控制语句可以控制循环体内语句的执行过程。

GO 语言支持以下几种循环控制语句:

控制语句描述
break 语句经常用于中断当前 for 循环或跳出 switch 语句
continue 语句跳过当前循环的剩余语句,然后继续进行下一轮循环。
goto 语句将控制转移到被标记的语句。
package main
​
import "fmt"func main() {
​
    i := 1
    for {  // for里面什么都不写代表死循环
        fmt.Println("loop")
        break
    }
    for j := 7; j < 9; j++ {
        fmt.Println(j)
    }
​
    for n := 0; n < 5; n++ {
        if n%2 == 0 {
            continue
        }
        fmt.Println(n)
    }
    for i <= 3 {
        fmt.Println(i)
        i = i + 1
    }
}

1.4 基础语法-switch

switch 语句用于基于不同条件执行不同动作。

go语言里面的switch分支结构。看起来也C或者C++比较类似。同样的在switch后面的那个变量名,并不是要括号。

这里有个很大的一点不同的是,在c++里面,switch case如果不显示加break的话会然后会继续往下跑完所有的case,在go语言里面的话是不需要加break的。

相比C或者C++,go语言里面的switch功能更强大。可以使用任意的变量类型,甚至可以用来取代任意的if else语句。你可以在switch后面不加任何的变量,然后在case里面写条件分支。这样代码相比你用多个if else代码逻辑会更为清晰。

package main
​
import (
    "fmt"
    "time"
)
​
func main() {
​
    a := 2
    switch a {
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    case 4, 5:
        fmt.Println("four or five")
    default:
        fmt.Println("other")
    }
​
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }
}
// 输出:
two
It's after noon

1.5 基础语法-数组

Go 语言提供了数组类型的数据结构。数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。

数组就是一个具有编号且长度固定的元素序列。比如这里的话是一个可以存放5个int元素 对于一个数组,可以很方便地取特定索引的值或者往特定索引取存储值,然后也能够直接去打印一个数组。不过,在真实业务代码里面,我们很少直接使用数组,因为它长度是固定的,我们用的更多的是切片。

package mainimport "fmt"
​
func main() {
​
    var a [5]int
    a[4] = 100
    fmt.Println("get:", a[2])
    fmt.Println("len:", len(a))
​
    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println(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)
}

// 输出
get: 0
len: 5                
[1 2 3 4 5]           
2d:  [[0 1 2] [1 2 3]]

2.7基础语法-切片

Go 语言切片是对数组的抽象。Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

我们可以用make来创建一个切片, 可以像数组一样去取值,使用append来追加元素。

注意append的用法的话,你必须把append的结果赋值为原数组。 因为slice的原理实际上是它有一个它存储了一个长度和一个容量,加一个指向一个数组的指针,在你执行append操作的时候,如果容量不够的话,会扩容并且返回新的slice。

slice 初始化的时候也可以指定长度。slice 拥有像python 一样的切片操作,比如这个代表取出第二个到第五个位置的元素, 不包括第五个元素。不过不同于python,这里不支持负数索引。

package main
​
import "fmt"func main() {
​
    s := make([]string, 3)
    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    fmt.Println("get:", s[2])   // c
    fmt.Println("len:", len(s)) // 3
​
    s = append(s, "d")
    s = append(s, "e", "f")
    fmt.Println(s) // [a b c d e f]
​
    c := make([]string, len(s))
    copy(c, s)
    fmt.Println(c) // [a b c d e f]
​
    fmt.Println(s[2:5]) // [c d e]
    fmt.Println(s[:5])  // [a b c d e]
    fmt.Println(s[2:])  // [c d e f]
​
    good := []string{"g", "o", "o", "d"}
    fmt.Println(good) // [g o o d]
}

1.6 基础语法-map

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

map,在其他编程语言里面, 它可能可以叫做哈希或者字典。map 是实际使用过程中最频繁用到的数据结构。我们可以用make来创建一个空 map,这里会需要两个类型,第一个是那个key的类型,这里是string另一个是value 的类型,这里是int。可以从里面去存储或者取出键值对。可以用delete从里面删除键值对。

golang的map是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。

package main
​
import "fmt"func main() {
    m := make(map[string]int)
    m["one"] = 1
    m["two"] = 2
    fmt.Println(m)           // map[one:1 two:2]
    fmt.Println(len(m))      // 2
    fmt.Println(m["one"])    // 1
    fmt.Println(m["unknow"]) // 0
​
    r, ok := m["unknow"]
    fmt.Println(r, ok) // 0 falsedelete(m, "one")
​
    m2 := map[string]int{"one": 1, "two": 2}
    var m3 = map[string]int{"one": 1, "two": 2}
    fmt.Println(m2, m3)
}

1.7 基础语法-range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。

用range来快速遍历,这样代码能够更加简洁。 range遍历的时候,对于数组会返回两个值,第一个是索引,第二个是对应位置的值。如果我们不需要索引的话,我们可以用下划线来忽略。

package main
​
import "fmt"func main() {
    nums := []int{2, 3, 4}
    sum := 0
    for i, num := range nums {
        sum += num
        if num == 2 {
            fmt.Println("index:", i, "num:", num) // index: 0 num: 2
        }
    }
    fmt.Println(sum) // 9
​
    m := map[string]string{"a": "A", "b": "B"}
    for k, v := range m {
        fmt.Println(k, v) // b 8; a A
    }
    for k := range m {
        fmt.Println("key", k) // key a; key b
    }
}

1.8 基础语法-函数

Go 语言最少有个 main() 函数(java叫做方法)。你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务。函数声明告诉了编译器函数的名称,返回类型,和参数。

Go 语言标准库提供了多种可动用的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。

package main
​
import "fmt"func add(a int, b int) int {
    return a + b
}
​
func add2(a, b int) int {
    return a + b
}
​
func exists(m map[string]string, k string) (v string, ok bool) {
    v, ok = m[k]
    return v, ok
}
​
func main() {
    res := add(1, 2)
    fmt.Println(res) // 3
​
    v, ok := exists(map[string]string{"a": "A"}, "a")
    fmt.Println(v, ok) // A True
}

这个是Golang里面一个简单的实现两个变量相加的函数。Golang 和其他很多语言不一样的是,变量类型是后置的。 Golang里面的函数原生支持返回多个值。在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个值是一个错误信息。

1.9 基础语法-指针

我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

package main
​
import "fmt"func add2(n int) {
    n += 2
}
​
func add2ptr(n *int) {
    *n += 2
}
​
func main() {
    n := 5
    add2(n)
    fmt.Println(n) // 5
    add2ptr(&n)
    fmt.Println(n) // 7
}

go里面也支持指针。指针的一个主要用途就是对于传入参数进行修改。 我们来看这个函数。这个函数试图把一个变量+2。 但是单纯像上面这种写法其实是无效的。因为传入函数的参数实际上是一个拷贝, 那也说这个+2,是对那个拷贝进行了+2,并不起作用。 如果我们需要起作用的话,那么我们需要把那个类型写成指针类型,那么为了类型匹配,调用的时候会加一个&符号。

1.10 基础语法-结构体

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

package main
​
import "fmt"type user struct {
    name     string
    password string
}
​
func main() {
    a := user{name: "wang", password: "1024"}
    b := user{"wang", "1024"}
    c := user{name: "wang"}
    c.password = "1024"
    var d user
    d.name = "wang"
    d.password = "1024"
​
    fmt.Println(a, b, c, d)                 // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
    fmt.Println(checkPassword(a, "haha"))   // false
    fmt.Println(checkPassword2(&a, "haha")) // false
}
​
func checkPassword(u user, password string) bool {
    return u.password == password
}
​
func checkPassword2(u *user, password string) bool {
    return u.password == password
}

结构体的话是带类型的字段的集合。 比如这里user结构包含了两个字段,name和password。我们可以用结构体的名称去初始化一个结构体变量,构造的时候需要传入每个字段的初始值。也可以用这种键值对的方式去指定初始值,这样可以只对一部分字段进行初始化。同样的结构体我们也能支持指针,这样能够实现对于结构体的修改,也可以在某些情况下避免一些大结构体的拷贝开销。

1.11 基础语法-结构体方法

在Golang里面可以为结构体去定义一些方法。 会有一点类似其他语言里面的类成员函数。 如这里,我们把上面一个例子的checkPassword的实现,从一个普通函数,改成了结构体方法。这样用户可以像a.checkPassword("xx”) 这样去调用。具体的代码修改,就是把第一个参数, 加上括号,写到函数名称前面。

在实现结构体的方法的时候也有两种写法,一种是带指针, 一种是不带指针。这个它们的区别的话是说如果你带指针的话,那你那么你就可以对这个结构体去做修改。如果你不带指针的话,那你实际上操作的是一个拷贝, 你就无法对结构体进行修改。

package main
​
import "fmt"type user struct {
    name     string
    password string
}
​
func (u user) checkPassword(password string) bool {
    return u.password == password
}
​
func (u *user) resetPassword(password string) {
    u.password = password
}
​
func main() {
    a := user{name: "wang", password: "1024"}
    a.resetPassword("2048")
    fmt.Println(a.checkPassword("2048")) // true
}

1.12 基础语法-错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error类型是一个接口类型,这是它的定义:

type error interface {
    Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:

result, err:= Sqrt(-1)
​
if err != nil {
   fmt.Println(err)
}

错误处理在go语言里面符合语言习惯的做法就是使用一个单独的返回值来传递错误信息。

不同于Java自家使用的异常。go语言的处理方式,能够很清晰地知道哪个函数返回了错误,并且能用简单的if else来处理错误。 在函数里面,我们可以在那个函数的返回值类型里面,后面加一个error,就代表这个函数可能会返回错误。

那么在函数实现的时候,return 需要同时return 两个值,要么就是如果出现错误的话,那么可以return nil 和一个error。如果没有的话,那么返回原本的结果和nil

package main
​
import (
    "errors"
    "fmt"
)
​
type user struct {
    name     string
    password string
}
​
func findUser(users []user, name string) (v *user, err error) {
    for _, u := range users {
        if u.name == name {
            return &u, nil
        }
    }
    return nil, errors.New("not found")
}
​
func main() {
    u, err := findUser([]user{{"wang", "1024"}}, "wang")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(u.name) // wangif u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
        fmt.Println(err) // not found
        return
    } else {
        fmt.Println(u.name)
    }
}

1.13 基础语法-字符串操作

下面我们来看go语言里面的字符串操作。在标准库string包里面有很多常用的字符串工具函数,比如contains判断个字符串里面是否有包含另个字符串 ,count 字符审计数,index 查找某个字符审的位置。join 连接多个字符串

package main
​
import (
    "fmt"
    "strings"
)
​
func main() {
    a := "hello"
    fmt.Println(strings.Contains(a, "ll"))                // true
    fmt.Println(strings.Count(a, "l"))                    // 2   统计
    fmt.Println(strings.HasPrefix(a, "he"))               // true
    fmt.Println(strings.HasSuffix(a, "llo"))              // true
    fmt.Println(strings.Index(a, "ll"))                   // 2
    fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo  拼接
    fmt.Println(strings.Repeat(a, 2))                     // hellohello  
    fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo  
    fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]  分割
    fmt.Println(strings.ToLower(a))                       // hello  转小写
    fmt.Println(strings.ToUpper(a))                       // HELLO  转大写
    fmt.Println(len(a))                                   // 5      长度
    b := "你好"
    fmt.Println(len(b)) // 6
}

1.14 基础语法-字符串格式化

标准库的FMT包里面有很多的字符串格式相关的方法,比如prinf这个类似于C语言里面的printf 函数。不同的是,在go语言里面的话, 你可以很轻松地用%v来打印任意类型的变量,而不需要区分数字符串。也可以用%+v打印详细结果,%#v则更详细。

package main
​
import "fmt"type point struct {
    x, y int
}
​
func main() {
    s := "hello"
    n := 123
    p := point{1, 2}
    fmt.Println(s, n) // hello 123
    fmt.Println(p)    // {1 2}
​
    fmt.Printf("s=%v\n", s)  // s=hello
    fmt.Printf("n=%v\n", n)  // n=123
    fmt.Printf("p=%v\n", p)  // p={1 2}
    fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
    fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
​
    f := 3.141592653
    fmt.Println(f)          // 3.141592653
    fmt.Printf("%.2f\n", f) // 3.14
}
​

1.15 基础语法-JSON处理

下面我们来看一下JSON操作,go语言里面的JSON操作非常简单,对于一个已有的结构体,我们可以什么都不做,只要保证每个字段的第一个字母是大写,也就是是公开字段。那么这个结构体就能用JSON.marshaler去序列化,变成一个JSON的字符串。

序列化之后的字符串也能够用JSON.Unmarshal去反序列化到一个空的变量里面。

这样默认序列化出来的字符串的话,它的风格是大写字母开头,而不是下划线。我们可以在后面用json tag等语法来去修改输出JSON结果里面的字段名。

package main
​
import (
    "encoding/json"
    "fmt"
)
​
type userInfo struct {
    Name  string
    Age   int `json:"age"`
    Hobby []string
}
​
func main() {
    a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
    buf, err := json.Marshal(a)
    if err != nil {
        panic(err)
    }
    fmt.Println(buf)         // [123 34 78 97...]
    fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
​
    buf, err = json.MarshalIndent(a, "", "\t")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf))
​
    var b userInfo
    err = json.Unmarshal(buf, &b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}

1.16 基础语法-时间处理

在go语言里面最常用的就是time.Now()来获取当前时间,然后你也可以用time.Date()去构造一个带时区的时间, 构造完的时间。上面有很多方法来获取这个时间点的年月日 小时分钟秒,然后也能用点sub去对两个时间进行减在和某些系统交互的时候,我们经常会用到时间戳。可以用.UNIX来获取时间戳time.format()、 time.Parse()

package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    now := time.Now()
    fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
    t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
    t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
    fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTC
    fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
    fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36
    diff := t2.Sub(t)
    fmt.Println(diff)                           // 1h5m0s
    fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
    t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
    if err != nil {
        panic(err)
    }
    fmt.Println(t3 == t)    // true
    fmt.Println(now.Unix()) // 1648738080
}

1.17 基础语法-数字解析

字符串和数字之间的转换。在go语言当中,关于字符串和数字类型之间的转换都在strconv这个包下,这个包是string convert这两个单词的缩写。 可以用strconv.ParseInt()或者strconv.ParseFloat()来解析一个字符串。可以用strconv.Atoi("AAA")把一个十进制字符串转成数字。可以用strconv.Itoa()把数字转成字符串。如果输入不合法,那么这些函数都会返回error

package main
​
import (
    "fmt"
    "strconv"
)
​
func main() {
    f, _ := strconv.ParseFloat("1.234", 64)
    fmt.Println(f) // 1.234
​
    n, _ := strconv.ParseInt("111", 10, 64)
    fmt.Println(n) // 111
​
    n, _ = strconv.ParseInt("0x1000", 0, 64)
    fmt.Println(n) // 4096
​
    n2, _ := strconv.Atoi("123")
    fmt.Println(n2) // 123
    n2, err := strconv.Atoi("AAA")
    fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
    n3 := strconv.Itoa(123)
    fmt.Println(n3) // 123
​
}

1.18 基础语法-进程信息

在go里面,我们能够用os.Args来得到程序执行的时候的指定的命令行参数。比如我们编译的一一个二进制文件,command。 后面接 abcd来启动,输出就是os.Args会是一个长度为 5的slice ,第一个成员代表二 进制自身的名字。我们可以用os.Getenv("PATH")来读取环境变量。

package main
​
import (
    "fmt"
    "os"
    "os/exec"
)
​
func main() {
    // go run example/20-env/main.go a b c d
    fmt.Println(os.Args) // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
    fmt.Println("-----------")
    fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
    fmt.Println("-----------")
    fmt.Println(os.Setenv("AA", "BB"))
    fmt.Println("-----------")
​
    buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf)) // 127.0.0.1       localhost
}