Skip to content

程序结构

名称

  • 函数、变量、常量、类型、语句标签和包都遵循:字母或下划线开头,后面跟随任意数量的字符、数字和下划线,并且区分大小写
  • 若实体在函数中声明,那么实体只在函数局部有效。如果声明在函数外,将对包内所有源文件可见
  • 实体的第一个字母的 大小写 决定其可见性是否跨包,大写表示对包外是可见和可访问的

声明

go
package main
import "fmt"
const commonConstant = "common"

func main() {
    var a = commonConstant
    var b string        // 会默认初始化为 ""
    var c string = "c"
    var d = "d"
    e := "d"            // 短变量声明
    x, y := true, 2.3   // 一次定义多个变量
    fmt.Println(a)      // common
    fmt.Println(b)      // ""
    fmt.Println(c)      // c
    fmt.Println(d)      // d
    fmt.Println(e)      // e
    fmt.Println(x)
    fmt.Println(y)
}

定义变量时如果不指定初始值,那么默认初始化为零值(int 为 0,string 为 "" ...)

变量

指针

指针的值是一个 变量的地址。使用指针可以在无须知道变量名称的情况下,简洁读取或更新变量的值。

go
package main
import "fmt"

func pointer() {
    a := "1"
    p := &a             // &a 表示获取一个指向整型变量的指针,类型是整型指针 (*int)
    fmt.Println(*p)     // *p 表示 p 指向的变量
    *p = "2"
    fmt.Println(a)      // 2

    var x, y int
    fmt.Println(&x == &x, &x == &y, &x == nil)  // true false false

    q := 1
    incr(&q)            // &q 所指向的值加 1,不依赖返回值就能实现更新
    fmt.Println(q)      // 2
    fmt.Println(&q)     // 指针地址
    fmt.Println(incr(&q))  // 3
}

func incr(p *int) int {
    *p++  // 递增 p 所指向的值,p 本身不变(p 是一个指针地址)
    return *p
}

func flagTest() {
    var str string
    // 传入变量的指针,不需要返回值就能改变 str 的值
    flag.StringVar(&str, "s", "", "将空格替换为指定分隔符")
    flag.Parse()  // 解析用户传入的命令行参数
    if str != "" {
        fmt.Println(strings.Join(flag.Args(), str))  // 参数是 ./main -s / a bc 输出 a/bc
    } else {
        fmt.Println(flag.Args())
    }
}
  1. &z 表示获取一个指向整型变量 z 的指针,类型是整型指针 (*int)
  2. *p 表示指针 p 指向的变量,*p 代表一个变量。而 p 则代表指针,是一个 0x 开头的地址
  3. 为什么使用指针?
    • 相比 Java 中都是 值传递(对形参的修改不会影响实参),通过传递指针而不是复制整个数据结构,可以节省时间和内存
    • 将变量的指针传递给函数,可以 在函数内部直接修改变量的值,而不需要返回值

new

通过 new(type) 方式创建变量的指针,常用于 初始化复杂类型的指针,便于在堆上分配内存。

go
package main
import "fmt"

func main() {
    p := new(int)
    x := new(int)
    fmt.Println(p)       // 指针的地址
    fmt.Println(p == x)  // 每 new 一次,每次的地址都不同
    fmt.Println(*p)      // 初始为 0

    p := new(Person)
    fmt.Println(p.Age)   // 会被初始化为零值
}

type Person struct {
    Name string
    Age  int
}

new 创建的变量是被初始化为 零值 的,Person 中的 Name 和 Age 分别被初始化为 "" 和 0。

变量的生命周期

  • 生命周期:程序执行过程中变量存在的时间段
  • 包级别的变量:生命周期伴随着 整个程序 的执行时间
  • 局部变量:每次执行声明语句时创建一个新的实体,一直存在到 不可访问

通过可达性算法来判断变量是否能够回收。与 JVM 从根对象 出发判断类似,将 包级别的变量 当前执行函数的局部变量 作为源头,通过 指针或其他方式 的引用可以找到的变量则被视为可达。

go
package main

var global *int

func f() {
    var x int
    x = 1
    global = &x
}

func g() {
    y := new(int)
    *y = 1
}
  1. 针对 f() 函数,尽管在其返回后还能通过 global 访问,这种情况成为 x 从 f 中逃逸
  2. g() 函数返回时,*y 变得不可访问,可以被回收。这种情况被 *y 无法从 g 中逃逸

赋值

go
func assign() {
    var x int
    x = 1  // 有名称的变量
    fmt.Printf("x: %d\n", x)

    y := &x
    *y = 2  // 通过指针间接赋值
    fmt.Printf("*y: %d\n", *y)

    p := new(Person)
    p.Name = "lucy"  // 结构体成员
    fmt.Printf("name: %s\n", p.Name)

    s := []int{1, 2, 3}  // 数组或 slice 或 map 的元素
    fmt.Printf("slice: %d\n", s[1])

    c := 1
    c++
    c--
    fmt.Printf("c: %d\n", c)

    a := 0
    b := 1
    a, b, c = b, a, a+b  // 多重赋值,即一次性赋值多个变量,并且变量支持右侧表达式推演
    fmt.Printf("a: %d, b: %d, c: %d\n", a, b, c)
}
  1. 有名称的变量赋值
  2. 通过指针间接赋值
  3. 结构体成员赋值
  4. 数组或 slice 或 map 元素赋值
  5. 多重赋值,一次赋值多个变量(左侧的变量个数需要和函数的返回值一样多,不需要的值赋值给 空标识符_

包和文件

在 Go 语言中包的作用和其他语言的库或模块作用类似,用于支持 模块化、封装、编译隔离和重用

bash
go mod init <module name>  # 初始化 module
go mod tidy                # 整理依赖

目录结构如下:

bash
.
├── README.md
├── src
   └── source.go
├── go.mod
├── main.go
└── chapter1
    ├── README.md
    └── chapter1.go
go
package main

import (
    "go-in-action/chapter1"  // <module name>/<file name>
)

func main() {
    chapter1.Main()
}
  1. 大写开头的 func 才能够被其他 Go 文件引用
  2. import 引入的路径只需要包含文件夹,不需要包含文件名称