Go语言基础语法快速入门 | 青训营笔记

55 阅读9分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 1 天

HelloWorld

package main // go app 都有一个 main 包import (
    "fmt" // 引入标准库的 fmt 包 (format, 格式化输出)
)
​
// go app 入口函数
func main() {
    fmt.Println("Hello World)
}

编译:编译成纯二进制文件,直接运行二进制文件即可,不需要其他任何依赖(不像Java还要拖一个JVM)

go build xxx.go # 编译
./xxx # 运行

直接运行:

go run xxx.go # 编译成临时二进制文件后运行, 体量比 go build 小, 速度比 go build 快

变量与数据类型

变量声明与赋值:

// 声明
var a int
// 赋值
a = 10
// 声明同时赋值, var 变量名 变量类型 = 变量值
var a int = 10
// 或者 自动判断数据类型
var a = 10
// 声明并赋值,省略 var
a := 10

数据类型:intfloatfloatboolstring

var a = "initial"var b, c int = 1, 2var d = truevar e float64
​
f := float32(e) // 强制类型转换
​
g := a + "foo"

常量:

const s string = "constant"
const h = 5000
const l = 12.34

分支语句

与Java不同点:if后没有小括号(),必须有花括号{},即不能写成一行。

if 7 % 2 == 0 {
    fmt.Println("7 % 2 == 0")
} else {
    fmt.Println("7 % 2 != 0")
}

Go 的if语句可以在判断前添加一个执行语句:

if n := 9; n < 10 {
    // do some ...
} else if (n > 0) {
    // ...
} else {
    // ...
}

Go 中的switch结构,某个case后不加break也不会一直往下执行,这是与 C/C++/Java 不同的地方。

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")
        default:
            fmt.Println("other")
    }
​
    // switch 的功能更加强大
    t := time.Now()
    switch {
        case t.Hour() < 12:
            fmt.Println("It's before noon")
        default:
            fmt.Println("It's after noon")
    }
}

循环

Go中只有for循环,没有whiledo while循环。

// 死循环
for {
    
}
​
// 经典 for 循环
for i := 0; i < 10; i++ {
    fmt.Println("i: ", i)
}
​
// while 循环的效果
i := 0
for i < 3 {
    // do some ...
    i++
}
​
for j := 0; j++ {
    // 死循环, j 一直加
    fmt.Println("j", j)
}

数组

var a [5]int // 声明 5 个元素,类型为 int 的数组
a[4] = 100 // 操作数组元素// 声明并赋值
b := [5]int{1, 2, 3, 4, 5}
​
var c [2][3]int // 二维数组

实际开发中很少使用长度固定的数组,更多的使用的是动态的切片。

切片(Slice)

就像 Java 的ArrayList<>,Python 的list,底层是数组,但是可以动态改变长度。

s := make([]string, 3) // 使用 make , 基于一个 string 数组创建切片
s[0] = "a"
s[1] = "abc"
s[2] = "e"
​
s = append(s, "d") // 追加元素, 注意要赋值回来
s = append(s, "ef", "gh") // 追加多个元素
​
fmt.Println(s[1:4]) // 支持和 Python 一样的切片操作
fmt.Println(s[:4])
fmt.Println(s[2:])
// Tips:不支持负数从末尾开始索引

Map

m := make(map[string]int) // 使用 make 创建 map
m["one"] = 1
m["two"] = 2
fmt.Println(m["one"])
fmt.Println(len(m))
fmt.Println(m["abcd"]) // 没有对应的 key , 得到的是 int 默认值 0// 有对应 key 得到的是对应的 value 和 true , 否则得到的是类型默认值和 false
val, ok := m["abcd"]
​
delete(m, "one")
​
// 声明并赋值
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}

使用make可以预先分配内存空间,在可以预计需要存储的元素的数量的情况下使用make指定元素数量创建SliceMap可以减少因需要扩容而重新分配内存的次数,从而提高执行速度。

Range

用于快速遍历数组、slicemap

nums := []int{1, 2, 3, 4}
sum := 0
for i, n := range nums {
    sum += n
    fmt.Println(i) // 0 1 2 3
}
fmt.Println(sum) // 10// 使用下划线可以忽略前面的某个值
for _, n := range nums {
    fmt.Println(n)
}
​
m := map[string]int{"one": 1, "two": 2}
for k, v := range m {
    fmt.Println(k, v)
}
for k := range m {
    fmt.Println(k)
}
for _, v := range m {
    fmt.Println(v)
}
// PS: 遍历 Map 是完全无序的,顺序是随机的

函数

Go 的函数:

  • 类型是后置的
  • 可以返回多个值,通常第一个值是真实的返回值,第二个值返回可能出现的错误
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
}

指针

  • 基本数据类型,变量存储的就是值,也叫值类型
  • 获取变量所在的内存空间的地址,用 & ,例如:
var i int = 10
// 获取 i 的地址:&i
fmt.Println("变量 i 在内存中的地址:", &i)// 0x140000a4008
  • 指针类型,变量存的是一个地址,这个地址指向的内存空间存的才是变量的值,例如:
var ptr *int = &i
// 1、ptr 是指针变量
// 2、ptr 的类型是 *int
// 3、ptr 的值是变量 i 在内存中的地址
​
fmt.Println(ptr)// 0x140000a4008
fmt.Println(&ptr)// 0x1400009e020

  • 获取指针类型所指向的值,使用:* ,例如:
// ptr 指向的值
fmt.Printf("ptr指向的值:", *ptr)  // 10

关于方法的值传递,进一步理解指针:

func add(n int) {
    n += 2
}
​
func add2(n *int) {
    *n += 2
}
​
func main() {
    n := 1
    add(n)
    fmt.Println(n) // 1
    add2(&n)
    fmt.Println(n) // 3
}

结构体

带字段的类型的集合。类似面向对象语言中的类,C中的结构体。

package main
​
import "fmt"type user struct {
    id string
    name string
}
​
func main() {
    u1 := user{id: "1", name: "zhangsan"}
    u2 := user{"2", "lisi"}
    u3 := user{name: "wangwu"} // 未初始化的字段则会指定为默认值
    u3.id = "3"
    var u4 user // 声明 user 类型的变量 u4
    u4.id = "4"
    u4.name = "zhaoliu"
    fmt.Println(u1, u2, u3, u4) // {1 zhangsan} {2 lisi} {3 wangwu} {4 zhaoliu}
    fmt.Println(u1.name) // zhangsan
    // ...
}

结构体方法,类似面向对象语言中的类的成员方法。

package main
​
import "fmt"type user struct {
    id string
    name string
}
​
func (u user) sayHello() {
    fmt.Println("My name is ", u.name, ", my id is ", u.id)
}
​
func main() {
    u1 := user{id: "1", name: "zhangsan"}
    u1.sayHello()
}

带指针的结构体方法,注意修改结构体内容必须是带指针的结构体方法,与面向对象的类成员方法不同:

package main
​
import "fmt"type user struct {
    id string
    name string
}
​
func (u user) setName1(name string) {
    u.name = name
}
​
func (u *user) setName2(name string) {
    u.name = name
}
​
func main() {
    u1 := user{id: "1", name: "zhangsan"}
    u1.setName1("lisi")
    fmt.Println(u1) // {1 zhangsan}
    u1.setName2("wangwu")
    fmt.Println(u1) // {1 wangwu}
}

错误处理

Go 没有try...catch...,Go的错误处理是由函数主动通过第二个返回值返回是否有错误信息。

package main
​
import (
  "errors"
  "fmt"
)
​
// 返回值类型中加上了error类型,即代表这个函数可能出现错误
// 返回的时候要返回两个值,其中err在函数体内直接使用即可,而不需要主动声明
func hasNum(nums []int, target int) (index int, err error) {
  idx := -1
  for i, n := range nums {
    if n == target {
      idx = i
      break
    }
  }
  if idx == -1 {
    err = errors.New("not found")
  }
  return idx, err
}
​
func main() {
  nums := []int{1, 2, 3, 4, 5}
  idx1, err1 := hasNum(nums, 4) // 3 <nil>
  fmt.Println(idx1, err1)
  idx2, err2 := hasNum(nums, 6) // -1 not found
  fmt.Println(idx2, err2)
    if err2 != nil {
        // 错误处理...
    }
}

简单错误是仅出现一次的错误,且在其他地方不需要捕获该错误。对于简单错误,直接这样通过errors.New来创建即可,如果有格式化需求,则可以使用fmt.Errorf

对于复杂的错误,可以采用错误的WrapUnWrap。错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而形成一个error的跟踪链。具体操作为在fmt.Errorf中使用%w将一个错误关联至错误链中。

func Foo() (err error) {
    // do some...
    res, err2 := OtherFoo()
    if err2 != nil {
        return fmt.Errorf("has xxx error: %w", err2)
    }
}

对于判断错误是否为特定错误,可以使用errors.Is

data, err = lockedfile.Read(targ)
if errors.Is(err, fs.ErrNotExist) {
    return []byte{}, nil
}
return data, err

在错误链上获取特定种类的错误,使用errors.As

if _, err := os.Open("non-existing"); err != nil {
    var pathError *fs.PathError
    if errors.As(err, &pathError) {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}

当程序真正发生不可逆转的错误,导致程序已经无法正常工作时,使用panic

res, err := Foo()
if err != nil {
    // panic(err)
    // 打上日志再 panic(err)
    log.Panicf("Error:%v", err)
}

recover函数:

  • 是在defer中的内置函数。
  • 只能在被defer的函数中使用。
  • defer是函数最后执行的函数。
  • 嵌套无法生效。
  • 只在当前协程生效。
  • 多个defer语句的顺序是后进先出。
  • recover会使程序从panic中恢复,导致panic异常的函数不会继续执行,但能正常返回。
  • defer定义在panic函数之前,因为panic之后的内容都不会执行,包括之后的defer
  • recover的返回值为panic的错误,如果没有发生panic,则会返回nil
func Foo() (err error) {
    defer func() {
        // 当 Foo 函数中出现 panic 时,调用 recover 函数就会获取到错误
        // debug.Stack() 可以记录调用栈
        if e := recover(); e != nil {
            err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack())
        }
    }
}

字符串操作

strings包中有很多字符串操作函数。

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
    // 将 a 字符串重复两次得到一个新的字符串
    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, unicode一个中文由三个字符编码
}

字符串格式化

在 Java 中,System.out.printf(...)中可以用%d表示数字,字符串用%s等。而 Go 中可以用%v表示任意类型。

package main
​
import "fmt"type point struct {
    x, y int
}
​
func main() {
    s := "hello"
  n := 123
  p := point{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}
​
    // %+v 可以显示更详细的信息
  fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
    // %#v 可以显示比 +v 更详细的信息
    fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
}

JSON处理

只要保证结构体中每个字段的第一个字母是大写(Golang中的公开字段的表示方式(public)),就能通过encoding/json包下的Marshal方法序列化成Json格式的字符串的byte数组。

package main
​
import (
    "encoding/json"
    "fmt"
)
​
type userInfo struct {
  Name string
    Age int `json:"age"` // 指定序列化成Json时的key用age
    Hobby []string
}
​
func main() {
    a := userInfo{"zhangsan", 18, []string{"Golang", "Java"}}
    buf, err := json.Marshal(a)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(buf) // [123 34 78 97 109 101 34 58 34 122 104 ...]
    fmt.Println(string(buf)) // {"Name":"zhangsan","age":18,"Hobby":["Golang","Java"]}
​
    // 得到格式化(缩进、换行)的Json
  buf2, err := json.MarshalIndent(a, "", "\t")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf2))
  //{
    //    "Name": "zhangsan",
    //    "age": 18,
    //    "Hobby": [
    //            "Golang",
    //            "Java"
    //    ]
  //}
    
    
    // 反序列化成结构体对象
    var b userInfo
    err = json.Unmarshal(buf, &b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", b) // main.userInfo{Name:"zhangsan", Age:18, Hobby:[]string{"Golang", "Java"}}
}

时间处理

time包下的函数用于时间处理。

package main
​
import (
  "fmt"
  "time"
)
​
func main() {
  now := time.Now() // 获取当前时间
  fmt.Println(now)
  // 构造指定时间
  t := time.Date(2023, 1, 1, 12, 25, 36, 0, time.UTC)
  fmt.Println(t)
  // 获取时间信息
  fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute())
  // 格式化时间为 yyyy-MM-dd HH:mm:ss 格式, Format函数的参数是固定的
  fmt.Println(now.Format("2006-01-02 15:04:05"))
  // 时间减法
  diff := now.Sub(t)
  fmt.Println(diff, diff.Minutes(), diff.Seconds())
  // 指定时间格式和时间字符串, 将时间字符串转换为时间对象
  t2, err := time.Parse("2006-01-02 15:04:05", "2023-01-15 12:38:36")
  if err != nil {
    panic(err)
  }
  fmt.Printf("%#v", t2)
  // 获取时间戳
  fmt.Println("\n", now.Unix())
}

数字解析

strconv包下的函数用于字符串的数字解析。

package main
​
import (
    "fmt"
    "strconv"
)
​
func main() {
    f, _ = strconv.ParseFloat("1.234", 64) // 转为 float64
    n, _ = strconv.ParseInt("123", 10, 64) // 转为 10 进制, int64
    n2, _ = strconv.Atoi("356") // 自动转换
}

进程信息

os包下的函数用于获取进程信息。

package main
​
import (
    "fmt"
    "os"
    "os/exec"
)
​
func main() {
    // 命令行参数
    // go run xxxxxx.go args1 args2 ...
    fmt.Println(os.Args)
    // 环境变量
    fmt.Println(os.Getenv("PATH"))
    os.Setenv("AA", "BB")
    // 执行命令(启动子进程)
    buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf))
}