Go 语言语法概要

本文将概括 Go 语言的所有语法知识,形成一个简单的语法参考手册,如果你有什么语法不清楚,快速查看。

1. 第一个 go 程序

// 这一行声明了当前的 Go 程序属于 main 包。在 Go 语言中,每个可执行程序都
// 必须有一个 main 包,并且该包中必须有一个 main 函数作为程序的入口点
package main

// 这一行导入了 Go 标准库中的 fmt 包,它提供了格式化 I/O 功能,
// 包括 Println 函数,该函数用于打印一行文本到控制台
import "fmt"

// main 函数是Go程序的入口点
// 当你运行一个 Go 程序时,它会从 main 函数开始执行。
func main() {
    fmt.Println("Hello, World!")
}

// 编译并运行该程序:
// 要运行这段代码,你需要确保你已经安装了 Go 语言的运行环境,并且你可以将这段
// 代码保存为一个 .go 文件(例如 hello.go),然后在命令行中使用 go run hello.go 命令来运行它。

运行:

(1)直接使用 go run 命令运行,如下:

$ go run hello.go
Hello, World!

(2)使用 go build 命令将程序编译成可执行文件,如:

$ go build hello.go
$ hello.exe
Hello, World!

注意:

(1)可执行的 go 程序,必须存在 main 包,否则“package command-line-arguments is not a main package”错误;

(2)go 中,导入的包必须使用,否则出错:

# command-line-arguments
01-base\variable\variable1.go:3:8: imported and not used: "fmt"

2. go 基础语法

2.1. 注释

go 支持单行和多行注释:

// 单行注释
/*
多行注释
*/

注意:注释不能嵌套;

2.2. 标识符

标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母(A~Z和a~z)数字(0~9)、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。

2.3. 字符串连接

Go 语言的字符串连接可以通过 + 实现,如:

fmt.Println("Hello" + " " + "World")

2.4. 关键字

Go 中的 25 个关键字或保留字:

break

default

func

interface

select

case

defer

go

map

struct

chan

else

goto

package

switch

const

fallthrough

if

range

type

continue

for

import

return

var

Go 语言还有 36 个预定义标识符:

append

bool

byte

cap

close

complex

complex64

complex128

uint16


copy

false

float32

float64

imag

int

int8

int16

uint32


int32

int64

iota

len

make

new

nil

panic

uint64


print

println

real

recover

string

true

uint

uint8

uintptr


2.5. 空格

在 Go 语言中,空格通常用于分隔标识符、关键字、运算符和表达式,以提高代码的可读性。如下:

package main

import "fmt"

func main() {
	// 使用空格分隔语句的各个部分,否则导致语法错误
	var a int = 11
	// 在 > 符号前后添加空格,提升代码的可读性
	if a > 10 {
		fmt.Println("a > 10")
	}
}

2.6. 数据类型

2.6.1. 布尔类型

布尔型的值只可以是常量 true 或者 false,如:var b bool = true。

2.6.2. 数字类型

Go 语言支持整型(int)和浮点型(float)数字,并且支持复数,其中位的运算采用补码。

整型:

  • uint8 无符号 8 位整型 (0 到 255)

  • uint16 无符号 16 位整型 (0 到 65535)

  • uint32 无符号 32 位整型 (0 到 4294967295)

  • uint64 无符号 64 位整型 (0 到 18446744073709551615)

  • int8 有符号 8 位整型 (-128 到 127)

  • int16 有符号 16 位整型 (-32768 到 32767)

  • int32 有符号 32 位整型 (-2147483648 到 2147483647)

  • int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

浮点型:

  • float32 IEEE-754 32位浮点型数

  • float64 IEEE-754 64位浮点型数

  • complex64 32 位实数和虚数

  • complex128 64 位实数和虚数

其他数字类型:

  • byte 类似 uint8

  • rune 类似 int32

  • uint 32 或 64 位

  • int 与 uint 一样大小

  • uintptr 无符号整型,用于存放一个指针

2.6.3. 字符串类型

字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。如:

var s string = "hello world"

2.6.4. 派生类型

  • 指针类型(Pointer)

  • 数组类型

  • 结构化类型(struct)

  • Channel 类型

  • 函数类型

  • 切片类型

  • 接口类型(interface)

  • Map 类型

2.7. 类型转换

类型转换用于将一种数据类型的变量转换为另外一种类型的变量。格式如下:

type_name(expression)

type_name 为类型,expression 为表达式。例如:

// 将整型转换为浮点型
var a int = 10
var b float64 = float64(a) // 转换

// 将一个字符串转换成另一个类型
var str string = "10"
var num int
num, _ = strconv.Atoi(str) // 调用内置函数

// 将整型转换为字符串
var num int = 123
var str string = strconv.Itoa(num)

// 字符串转换为浮点数
var str string = "3.14"
num, err := strconv.ParseFloat(str, 64)

// 浮点数转换为字符串
var num float32 = 3.14
str := strconv.FormatFloat(num, 'f', 2, 64)

2.7.1. 接口类型转换

接口类型转换有两种情况:类型断言和类型转换。

类型断言用于将接口类型转换为指定类型,其语法为:

value.(type) 
//或者 
value.(T)

其中 value 是接口类型的变量,type 或 T 是要转换成的类型。如果类型断言成功,它将返回转换后的值和一个布尔值,表示转换是否成功。例如:

var i interface{} = "Hello, World"
str, ok := i.(string)
if ok {
    // 转换成功
} else {
    // 转换失败
}

2.8. 变量(variable)

变量是用来存放数据的临时容器,可以通过名字访问。go 语言中声明变量语法如下:

// 1.声明变量 a,类型为 string,初始值为 hello
var a string = "hello"

// 类型的默认值:
// - 数值类型(包括complex64/128)为 0
// - 布尔类型为 false
// - 字符串为 ""(空字符串)
// - 指针、数组、map、chan、函数、接口类型为 nil
// 2.声明变量 a,类型为 string,初始化值为默认值
var a string
// 3.一次声明两个变量 a 和 b,类型为 string,使用默认值初始化
var a, b string

// 4.声明变量 a,省略类型,由 go 根据设置的值推断类型
var a = "hello world"

// 5.短变量声明,常用于函数内部
v_name := value

// 类型相同多个变量, 非全局变量
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3

// 允许,因为 c 变量没有被声明
var a,b = 10,20
a,b,c := 100,200,300

// 多个变量声明,不需要显示声明类型,自动推断
var vname1, vname2, vname3 = v1, v2, v3

// 这种因式分解关键字的写法一般用于声明全局变量
var (
    vname1 v_type1
    vname2 v_type2
)

错误的声明:

// 错误:
// # command-line-arguments
// 01-base\variable\variable2.go:6:8: syntax error: unexpected newline, expecting type
var a
a = 10

// 错误:
// 这时候会产生编译错误,因为 intVal 已经声明,不需要重新声明
var intVal int 
intVal :=1

// 错误:
// 多个变量声明,不需要显示声明类型,自动推断
var vname1, vname2, vname3 = v1, v2, v3

// 注意:出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误
// 如果其中有一个或多个变量没有被声明,是可以的
var vname1, vname2, vname3 type
vname1, vname2, vname3 := v1, v2, v3

2.9. 常量(const)

常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

常量可以用 len(), cap(), unsafe.Sizeof() 函数计算表达式的值。常量表达式中,函数必须是内置函数。

// 语法
// const identifier [type] = value

// 显式类型定义,定义常量 b,类型为 string,值为 “abc” 字符串
const b string = "abc"
// 隐式类型定义
const b = "abc"

// 多个相同类型的声明可以简写
const c_name1, c_name2 = value1, value2

// 常量还可以用作枚举
const (
    Unknown = 0
    Female = 1
    Male = 2
)

2.9.1. iota

iota 是个特殊常量,可以认为是一个可以被编译器修改的常量。

iota 在 const 关键字出现时将被重置为 0 (const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次 (iota 可理解为 const 语句块中的行索引)。

const (
    a = iota   //0
    b          //1
    c          //2
    d = "ha"   //独立值,iota += 1
    e          //"ha"   iota += 1
    f = 100    //iota +=1
    g          //100  iota +=1
    h = iota   //7,恢复计数
    i          //8
)

2.9.2. 变量作用域

2.9.2.1. 局部变量

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数、形式参数和返回值变量也是局部变量。

如果全局变量和局部变量名称相同,则优先取局部变量的值。

例如:

func main() {
    /* 声明局部变量 */
    var a, b, c int

    /* 初始化参数 */
    a = 10
    b = 20
    c = a + b

    fmt.Printf ("结果: a = %d, b = %d and c = %d\n", a, b, c)
}
2.9.2.2. 全局变量

在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。

全局变量可以在任何函数中使用,例如:

package main

import "fmt"

/* 声明全局变量 */
var g int

func main() {
    /* 声明局部变量 */
    var a, b int

    /* 初始化参数 */
    a = 10
    b = 20
    g = a + b

    fmt.Printf("结果: a = %d, b = %d and g = %d\n", a, b, g)
}

2.10. 运算符

2.10.1. 算术运算符

假定 A 值为 10,B 值为 20。

  • + 相加,如:A + B = 30

  • - 相减,如:A - B = -10

  • * 相乘,如:A * B = 200

  • / 相除,如:B / A = 2

  • % 求余,如:B % A = 0

  • ++ 自增,如:A++ = 11

  • -- 自减,如:A-- = 9

2.10.2. 关系运算符

假定 A 值为 10,B 值为 20。

  • == 检查两个值是否相等,如果相等返回 True 否则返回 False。如:(A == B) 为 False

  • != 检查两个值是否不相等,如果不相等返回 True 否则返回 False。如:(A != B) 为 True

  • > 检查左边值是否大于右边值,如果是返回 True 否则返回 False。如:(A > B) 为 False

  • < 检查左边值是否小于右边值,如果是返回 True 否则返回 False。如:(A < B) 为 True

  • >= 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。如:(A >= B) 为 False

  • <= 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。如:(A <= B) 为 True

2.10.3. 逻辑运算符

假定 A 值为 True,B 值为 False。

  • && 逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。如:(A && B) 为 False

  • || 逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。如:(A || B) 为 True

  • ! 逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。如:!(A && B) 为 True

2.10.4. 位运算符

  • & 与

  • | 或

  • ^ 异或

p

q

p & q

p | q

p ^ q

0

0

0

0

0

0

1

0

1

1

1

1

1

1

0

1

0

0

1

1

假定 A 为60,B 为13。

  • & 按位与运算符"&"是双目运算符。 其功能是参与运算的两数各对应的二进位相与。如:(A & B) 结果为 12, 二进制为 0000 1100

  • | 按位或运算符"|"是双目运算符。 其功能是参与运算的两数各对应的二进位相或。如:(A | B) 结果为 61, 二进制为 0011 1101

  • ^ 按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。如:(A ^ B) 结果为 49, 二进制为 0011 0001

  • << 左移运算符"<<"是双目运算符。左移n位就是乘以2的n次方。 其功能把"<<"左边的运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补0。如: A << 2 结果为 240 ,二进制为 1111 0000

  • >> 右移运算符">>"是双目运算符。右移n位就是除以2的n次方。 其功能是把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数。 如:A >> 2 结果为 15 ,二进制为 0000 1111

2.10.5. 赋值运算符

  • = 简单的赋值运算符,将一个表达式的值赋给一个左值,如: C = A + B 将 A + B 表达式结果赋值给 C

  • += 相加后再赋值,如:C += A 等于 C = C + A

  • -= 相减后再赋值,如:C -= A 等于 C = C - A

  • *= 相乘后再赋值,如:C *= A 等于 C = C * A

  • /= 相除后再赋值,如:C /= A 等于 C = C / A

  • %= 求余后再赋值,如:C %= A 等于 C = C % A

  • <<= 左移后赋值,如:C <<= 2 等于 C = C << 2

  • >>= 右移后赋值,如:C >>= 2 等于 C = C >> 2

  • &= 按位与后赋值,如:C &= 2 等于 C = C & 2

  • ^= 按位异或后赋值,如:C ^= 2 等于 C = C ^ 2

  • |= 按位或后赋值,如:C |= 2 等于 C = C | 2

2.10.6. 其他运算符

  • & 返回变量存储地址,如:&a; 将给出变量的实际地址。

  • * 指针变量。如:*a; 是一个指针变量

2.11. 运算符优先级

优先级

运算符

5(高)

* / % << >> & &^

4

+ - | ^

3

== != < <= > >=

2

&&

1(低)

||

当然,你可以通过使用括号“()”来临时提升某个表达式的整体运算优先级。如:(1 + 5) * 100

3. 条件语句

3.1. if 语句

if 布尔表达式 {
    /* 在布尔表达式为 true 时执行 */
}

3.2. if...else 语句

if 布尔表达式 {
    /* 在布尔表达式为 true 时执行 */
} else {
    /* 在布尔表达式为 false 时执行 */
}

3.3. if 语句嵌套

if 布尔表达式 1 {
    /* 在布尔表达式 1 为 true 时执行 */
    if 布尔表达式 2 {
        /* 在布尔表达式 2 为 true 时执行 */
    }
}

3.4. switch 语句

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。

switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加 break。

switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough 。

Go 编程语言中 switch 语句的语法如下:

switch var1 {
    case val1:
    ...
    case val2:
    ...
    default:
    ...
}

例如:

var grade string = ""
var marks int = 90
switch marks {
    case 90: grade = "A"
    case 80: grade = "B"
    case 50,60,70 : grade = "C"
    default: grade = "D"  
}
// grade = A

3.5. Type Switch

switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。

Type Switch 语法格式如下:

switch x.(type){
    case type:
    statement(s);      
    case type:
    statement(s); 
    /* 你可以定义任意个数的case */
    default: /* 可选 */
    statement(s);
}

例如:

var x interface{}

switch i := x.(type) {
    case nil:  
    fmt.Printf(" x 的类型 :%T",i) // 这里被匹配                
    case int:  
    fmt.Printf("x 是 int 型")                      
    case float64:
    fmt.Printf("x 是 float64 型")          
    case func(int) float64:
    fmt.Printf("x 是 func(int) 型")                      
    case bool, string:
    fmt.Printf("x 是 bool 或 string 型" )      
    default:
    fmt.Printf("未知型")    
}  
// 输出:
// x 的类型 :<nil>

3.6. fallthrough

使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。例如:

switch {
    case false:
    fmt.Println("1、case 条件语句为 false")
    fallthrough
    case true:
    fmt.Println("2、case 条件语句为 true") // 被执行
    fallthrough
    case false:
    fmt.Println("3、case 条件语句为 false") // 被执行
    fallthrough
    case true:
    fmt.Println("4、case 条件语句为 true") // 被执行,这里没有 fallthrough
    case false:
    fmt.Println("5、case 条件语句为 false")
    fallthrough
    default:
    fmt.Println("6、默认 case")
}
//输出:
// 2、case 条件语句为 true
// 3、case 条件语句为 false
// 4、case 条件语句为 true

3.7. select 语句

select 是 Go 中的一个控制结构,类似于 switch 语句。

select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。

select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。

如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。

语法如下:

channel1 := make(chan string)
channel2 := make(chan string)

select {
    case <- channel1: // 从通道取值
    // 执行的代码
    case value := <- channel2: // 从通道取值
    // 执行的代码
    case channel3 <- value: // 写值到通道
    // 执行的代码
    // 你可以定义任意数量的 case
    default:
    // 所有通道都没有准备好,执行的代码
}

4. 循环语句

4.1. for 循环

For 循环有 3 种形式:

(1)形式一:for init; condition; post { }

sum := 0
for i := 0; i <= 10; i++ {
    sum += i
}
fmt.Println(sum)

(2)形式二:for condition { }

sum := 1
for ; sum <= 10; {
    sum += sum
}
fmt.Println(sum)

// 这样写也可以,更像 While 语句形式
for sum <= 10{
    sum += sum
}
fmt.Println(sum)

(3)形式三:for { } 死循环/无限循环

sum := 0
for {
    sum++ // 无限循环下去
}

// 或者

for true {
    sum++ // 无限循环下去
}

4.2. for 循环的 range 格式

range 格式的循环可以对字符串、数组、切片等进行迭代输出元素。如下:

strings := []string{"hello", "world"}
for i, s := range strings {
    fmt.Println(i, s)
}

// 输出索引,map 中为键
for key := range strings {
    fmt.Println(key)
}

// 输出元素
for _, s := range strings {
    fmt.Println(s)
}

注意:_ 是一个比较特殊的标记,用于忽略某个变量的值。

4.3. break 语句

在 Go 语言中,break 语句用于终止当前循环或者 switch 语句的执行,并跳出该循环或者 switch 语句的代码块。如下:

for i := 0; i < 10; i++ {
    if i == 5 {
        break // 当 i 等于 5 时跳出循环
    }
    fmt.Println(i)
}

带标记的 break,如:

re: // 定义一个标记
for i := 1; i <= 3; i++ {
    fmt.Printf("i: %d\n", i)
    for i2 := 11; i2 <= 13; i2++ {
        fmt.Printf("i2: %d\n", i2)
        break re // 直接退出到 re 标记
    }
}

在 switch 语句中使用 break:

day := "Tuesday"
switch day {
    case "Monday":
    fmt.Println("It's Monday.")
    case "Tuesday":
    fmt.Println("It's Tuesday.")
    break // 跳出 switch 语句
    case "Wednesday":
    fmt.Println("It's Wednesday.")
}

注意:在 Go 语言中,break 语句在 select 语句中的应用是相对特殊的。由于 select 语句的特性,break 语句并不能直接用于跳出 select 语句本身,因为 select 语句是非阻塞的,它会一直等待所有的通信操作都准备就绪。如果需要提前结束 select 语句的执行,可以使用 return 或者 goto 语句来达到相同的效果。

4.4. continue 语句

Go 语言的 continue 语句有点像 break 语句。但是 continue 不是跳出循环,而是跳过当前循环执行下一次循环语句。

for 循环中,执行 continue 语句会触发 for 增量语句的执行。

在多重循环中,可以用标号 label 标出想 continue 的循环。例如:

/* 定义局部变量 */
var a int = 10

/* for 循环 */
for a < 20 {
    if a == 15 {
        /* 跳过此次循环 */
        a = a + 1;
        continue;
    }
    fmt.Printf("a 的值为 : %d\n", a);
    a++;    
}

使用标记:

re: // 定义标记
for i := 1; i <= 3; i++ {
    fmt.Printf("i: %d\n", i)
    for i2 := 11; i2 <= 13; i2++ {
        fmt.Printf("i2: %d\n", i2)
        continue re // 使用标记,跳出外层本次循环
    }
}

4.5. goto 语句

Go 语言的 goto 语句可以无条件地转移到过程中指定的行。

goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。

语法:

goto label;
// ..
// 其他语句
// ..
label: statement;

示例:

/* 定义局部变量 */
var a int = 10

/* 循环 */
LOOP: for a < 20 {
    if a == 15 {
        /* 跳过迭代 */
        a = a + 1
        goto LOOP // 直接跳转到标记处
    }
    fmt.Printf("a的值为 : %d\n", a)
    a++    
}

5. 函数

函数是基本的代码块,用于执行一个任务。Go 语言最少有个 main() 函数。

5.1. 函数定义

函数定义格式如下:

func function_name( [parameter list] ) [return_types] {
    // 函数体
}

其中:

  • function_name  为函数名

  • parameter list  为函数参数列表,可以不指定

  • return_types  为函数返回类型,可以不指定

例如:定义 max 函数,用于计算两个值中的最大值,并返回。

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
    /* 声明局部变量 */
    var result int
    if (num1 > num2) {
        result = num1
    } else {
        result = num2
    }
    return result
}

5.2. 函数调用

调用上面定义的 max 函数:

/* 定义局部变量 */
var a int = 100
var b int = 200
var ret int

/* 调用函数并返回最大值 */
ret = max(a, b)

fmt.Printf( "最大值是 : %d\n", ret )

5.3. 函数返回多个值

Go 函数可以返回多个值,例如:

package main

import "fmt"

// 函数,接收两个字符串,返回两个字符串
func swap(x, y string) (string, string) {
    return y, x
}

func main() {
    // 调用函数
    a, b := swap("hello", "world")
    fmt.Println(a, b)
}
// 输出:
// world hello

5.4. 函数值传递值

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

5.5. 函数引用传递值

引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。例如:

/* 定义交换值函数,用了指针 */
func swap(x *int, y *int) {
    var temp int
    temp = *x    /* 保持 x 地址上的值 */
    *x = *y      /* 将 y 值赋给 x */
    *y = temp    /* 将 temp 值赋给 y */
}

5.6. 函数作为实参

Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。例如:

/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
    return math.Sqrt(x)
}

/* 使用函数 */
fmt.Println(getSquareRoot(9))

5.7. 函数闭包(匿名函数)

Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必声明。

匿名函数是一种没有函数名的函数,通常用于在函数内部定义函数,或者作为函数参数进行传递。

例如:

func getSequence() func() int {
    i:=0
    // 匿名函数,和 JS 的闭包类似
    return func() int {
        i+=1
        return i  
    }
}

5.8. 函数方法

一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:

func (variable_name variable_data_type) function_name() [return_type]{
    /* 函数体*/
}

其中:

  • variable_name  变量名

  • variable_data_type  变量类型

  • function_name  方法名

  • return_type  方法返回类型,可选

例如:

package main

import (
    "fmt"  
)

/* 定义结构体 */
type Circle struct {
    radius float64
}

func main() {
    // 声明结构体类型变量
    var c1 Circle
    // 给结构体类型变量中的元素赋值
    c1.radius = 10.00
    // 调用方法
    fmt.Println("圆的面积 = ", c1.getArea())
}

// 该方法属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
    //c.radius 即为 Circle 类型对象中的属性
    return 3.14 * c.radius * c.radius
}

6. 数组

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

数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。

6.1. 声明数组

语法格式如下:

var arrayName [size]dataType

其中,arrayName 是数组的名称,size 是数组的大小,dataType 是数组中元素的数据类型。如:

// 定义名为 balance 的数组,长度为 10,类型为 float32
var balance [10]float32

6.2. 初始化数组

// numbers 是大小为5的整数数组
// 在声明时,数组中的每个元素都会根据其数据类型进行默认初始化,对于整数类型,初始值为 0。
var numbers [5]int

// 使用初始化列表来初始化数组的元素
var numbers = [5]int{1, 2, 3, 4, 5}

// 使用 := 简短声明语法来声明和初始化数组
numbers := [5]int{1, 2, 3, 4, 5}

// 如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
//或
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

// 如果设置了数组的长度,我们还可以通过指定下标来初始化元素:
// 将索引为 1 和 3 的元素初始化
// 注意:{} 中元素个数不能大于 [] 中的数字
balance := [5]float32{ 1:2.0, 3:7.0 }

// 如果忽略 [] 中的数字不设置数组大小,将根据元素的个数来设置数组的大小
var a = []int{1,2,3,4}
fmt.Println(len(a)) // 4

注意:在 Go 语言中,数组的大小是类型的一部分,因此不同大小的数组是不兼容的,也就是说 [5]int 和 [10]int 是不同的类型。

6.3. 访问数组元素

数组元素可以通过索引(位置)来读取。例如:

// 读取 balance 数组第十个元素的值,因为索引从0开始
var salary float32 = balance[9]

// 设置 balance 数组第十个元素的值,因为索引从0开始
balance[9] = 3.14

6.4. 多维数组

Go 语言支持多维数组,以下为常用的多维数组声明方式:

var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type

以下实例声明了三维的整型数组:

var threedim [5][10][4]int

6.4.1. 二维数组

二维数组是最简单的多维数组,二维数组本质上是由一维数组组成的。二维数组定义方式如下:

var arrayName [ x ][ y ] variable_type

variable_type 为 Go 语言的数据类型,arrayName 为数组名,二维数组可认为是一个表格,x 为行,y 为列。

例如:

// Step 1: 创建数组
values := [][]int{}

// Step 2: 使用 append() 函数向空的二维数组添加两行一维数组
row1 := []int{1, 2, 3}
row2 := []int{4, 5, 6}
values = append(values, row1)
values = append(values, row2)

// Step 3: 显示两行数据
fmt.Println("Row 1")
fmt.Println(values[0])
fmt.Println("Row 2")
fmt.Println(values[1])

// Step 4: 访问第一个元素
fmt.Println("第一个元素为:")
fmt.Println(values[0][0])

// 输出:
// Row 1
// [1 2 3]
// Row 2
// [4 5 6]
// 第一个元素为:
// 1

6.4.2. 初始化二维数组

多维数组可通过大括号来初始值。以下实例为一个 3 行 4 列的二维数组:

a := [3][4]int{  
    {0, 1, 2, 3} ,   /*  第一行索引为 0 */
    {4, 5, 6, 7} ,   /*  第二行索引为 1 */
    {8, 9, 10, 11},   /* 第三行索引为 2 */
}

注意:以上代码中倒数第二行的 } 必须要有逗号,因为最后一行的 } 不能单独一行,也可以写成这样:

a := [3][4]int{  
    {0, 1, 2, 3} ,   /*  第一行索引为 0 */
    {4, 5, 6, 7} ,   /*  第二行索引为 1 */
    {8, 9, 10, 11}}   /* 第三行索引为 2 */

6.4.3. 访问二维数组

二维数组通过指定坐标来访问。如数组中的行索引与列索引,例如:

val := a[2][3]
//或
var value int = a[2][3]

以上实例访问了二维数组 val 第三行的第四个元素。

6.4.4. 向函数传递数组

Go 语言中的数组是值类型,因此在将数组传递给函数时,实际上是传递数组的副本。

如果你想向函数传递数组参数,你需要在函数定义时,声明形参为数组,我们可以通过以下两种方式来声明:

方式一:形参设定数组大小:

func myFunction(param [10]int) {
    ....
}

方式二:形参未设定数组大小:

func myFunction(param []int) {
    ....
}

例如:

/* 数组长度为 5 */
var  balance = [5]int {1000, 2, 3, 17, 50}
/* 数组作为参数传递给函数 */
avg := getAverage( balance, 5 )

// 函数
func getAverage(arr [5]int, size int) float32 {
    var i,sum int
    //...
    return avg;
}

如果你想要在函数内修改原始数组,可以通过传递数组的指针来实现。

7. 指针

一个指针变量指向了一个值的内存地址。类似于变量和常量,在使用指针前你需要声明指针。

指针声明格式如下:

var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。

以下是有效的指针声明:

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

7.1. 如何使用指针

指针使用流程:

  • 定义指针变量,如:var ip *int

  • 为指针变量赋值,使用 & 符号获取变量地址,如:ip = &a 将变量 a 的地址赋值给指针变量 ip

  • 访问指针变量中指向地址的值,在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。

例如:

package main

import "fmt"

func main() {
    var a int= 20   /* 声明实际变量 */
    var ip *int     /* 声明指针变量 */

    ip = &a  /* 指针变量的存储地址 */

    fmt.Printf("a 变量的地址是: %x\n", &a  ) // a 变量的地址是: 20818a220

    /* 指针变量的存储地址 */
    fmt.Printf("ip 变量储存的指针地址: %x\n", ip ) // ip 变量储存的指针地址: 20818a220

    /* 使用指针访问值 */
    fmt.Printf("*ip 变量的值: %d\n", *ip ) // *ip 变量的值: 20
}

7.2. 空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil。nil 指针也称为空指针。

空指针判断:

if(ptr != nil)    /* ptr 不是空指针 */
if(ptr == nil)    /* ptr 是空指针 */

7.3. 指针数组

以下声明了整型指针数组:

var ptr [MAX]*int;

ptr 为整型指针数组,因此每个元素都指向了一个值。

例如:将三个整数将存储在指针数组中:

package main

import "fmt"

const MAX int = 3

func main() {
    a := []int{10,100,200}
    var i int
    var ptr [MAX]*int;

    for  i = 0; i < MAX; i++ {
        ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
    }

    for  i = 0; i < MAX; i++ {
        fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
    }
}

7.4. 指向指针的指针

如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。

当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址。

指向指针的指针变量声明格式如下:

var ptr **int;

以上指向指针的指针变量为整型。访问指向指针的指针变量值需要使用两个 * 号,例如:

var a int
var ptr *int
var pptr **int

a = 3000

/* 指针 ptr 地址 */
ptr = &a

/* 指向指针 ptr 地址 */
pptr = &ptr

/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a )
fmt.Printf("指针变量 *ptr = %d\n", *ptr )
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)

7.5. 指针作为函数参数

Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。例如:

func main() {
    /* 定义局部变量 */
    var a int = 100
    var b int= 200
    swap(&a, &b);

    fmt.Printf("交换后 a 的值 : %d\n", a )
    fmt.Printf("交换后 b 的值 : %d\n", b )
}

/* 交换函数这样写更加简洁,也是 go 语言的特性,可以用下,c++ 和 c# 是不能这么干的 */
func swap(x *int, y *int){
    *x, *y = *y, *x
}

8. 结构体

Go 语言结构体中我们可以为不同项定义不同的数据类型。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。结构体表示一项记录,如下:

{
    "id": 100
    "name": "Tom",
    "age": 27
}

8.1. 定义结构体

结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:

type struct_variable_type struct {
    member definition
    member definition
    ...
    member definition
}

一旦定义了结构体类型,它就能用于变量的声明,语法格式如下:

variable_name := structure_variable_type {value1, value2...valuen}
// 或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

例如:

// 定义书籍结构体
type Books struct {
    title string
    author string
    subject string
    book_id int
}

func main() {
    // 创建一个新的结构体
    fmt.Println(Books{"Go语言", "作者1", "Go语言教程", 6495407})

    // 也可以使用 key => value 格式
    fmt.Println(Books{title: "Go语言", author: "作者1", subject: "Go语言教程", book_id: 6495407})

    // 忽略的字段为 0 或 空
    fmt.Println(Books{title: "Go语言", author: "作者1"})
}

8.2. 访问结构体成员

如果要访问结构体成员,需要使用点号 . 操作符,格式为:

结构体.成员名

例如:

type Books struct {
    title string
    author string
    subject string
    book_id int
}

func main() {
    var Book1 Books        /* 声明 Book1 为 Books 类型 */

    /* book 1 描述 */
    Book1.title = "Go 语言"
    Book1.author = "作者1"
    Book1.subject = "Go 语言教程"
    Book1.book_id = 6495407
}

8.3. 结构体作为函数参数

type Books struct {
    title string
    // author string
    // subject string
    // book_id int
}

func main() {
    var Book1 Books	/* 声明 Book1 为 Books 类型 */

    /* book 1 描述 */
    Book1.title = "Go 语言"

    /* 打印 Book1 信息 */
    printBook(Book1)
}

// 函数,参数为结构体
func printBook( book Books ) {
    fmt.Printf( "Book title : %s\n", book.title)
}

8.4. 结构体指针

你可以定义指向结构体的指针变量,格式如下:

var struct_pointer *Books

以上定义的指针变量可以存储结构体变量的地址。查看结构体变量地址,可以将 & 符号放置于结构体变量前:

struct_pointer = &Book1

使用结构体指针访问结构体成员,使用 "." 操作符:

struct_pointer.title

例如:

type Books struct {
    title string
    // author string
    // subject string
    // book_id int
}

func main() {
    var Book1 Books 	/* 声明 Book1 为 Books 类型 */

    /* book 1 描述 */
    Book1.title = "Go 语言"
    
    /* 打印 Book1 信息 */
    printBook(&Book1)
}
func printBook( book *Books ) {
    fmt.Printf( "Book title : %s\n", book.title)
}

9. 切片(Slice)

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

9.1. 定义切片

你可以声明一个未指定大小的数组来定义切片(切片不需要说明长度):

var identifier []type

// 使用 make() 函数来创建切片
var slice1 []type = make([]type, len)
// 也可以简写为
slice1 := make([]type, len)

// 也可以指定容量,其中 capacity 为可选参数
// 这里 len 是数组的长度并且也是切片的初始长度
make([]T, len, capacity)

9.2. 切片初始化

// 直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3
s :=[] int {1,2,3} 

// 初始化切片 s,是数组 arr 的引用
s := arr[:] 

// 将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:endIndex] 

// 将 arr 中从下标 startIndex 到最后一个元素创建为一个新的切片
s := arr[startIndex:] 

// 将 arr 中从下标 0 到 endIndex-1 下的元素创建为一个新的切片
s := arr[:endIndex] 

// 通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片
s :=make([]int,len,cap)

9.3. len() 和 cap() 函数

切片是可索引的,并且可以由 len() 方法获取长度。

切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

9.4. 空(nil)切片

一个切片在未初始化之前默认为 nil,长度为 0。

9.5. 切片截取

可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],例如:

/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}  

/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println(numbers[1:4])

/* 默认下限为 0,打印切片0到3的(不包含)*/
fmt.Println(numbers[:3])

/* 默认上限为 len(s),打印切片4到末尾 */
fmt.Println(numbers[4:])

9.6. append() 和 copy() 函数

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。

var numbers []int

/* 允许追加空切片 */
numbers = append(numbers, 0)

/* 向切片添加一个元素 */
numbers = append(numbers, 1)

/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)

/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)

10. 范围(Range)

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

for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:

// key 和 value 是可以省略
for key, value := range oldMap {
    newMap[key] = value
}

// 如果只想读取 key
for key := range oldMap
// 或者
for key, _ := range oldMap

// 如果只想读取 value
for _, value := range oldMap

// range 迭代字符串时,返回每个字符的索引和 Unicode 代码点(rune)
for i, c := range "hello" {
    fmt.Printf("index: %d, char: %c\n", i, c)
}
// 输出:
// index: 0, char: h
// index: 1, char: e
// ...

// range 遍历从通道接收的值,直到通道关闭
for v := range ch {
    fmt.Println(v)
}

11. Map(集合)

Map 是一种无序的键值对的集合。

Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,遍历 Map 时返回的键值对的顺序是不确定的。

在获取 Map 的值时,如果键不存在,返回该类型的零值,例如 int 类型的零值是 0,string 类型的零值是 ""。

Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量。

11.1. 定义 Map

// 使用内建函数 make 或使用 map 关键字来定义 Map
map_variable := make(map[KeyType]ValueType, initialCapacity)

其中 KeyType 是键的类型,ValueType 是值的类型,initialCapacity 是可选的参数,用于指定 Map 的初始容量。Map 的容量是指 Map 中可以保存的键值对的数量,当 Map 中的键值对数量达到容量时,Map 会自动扩容。如果不指定 initialCapacity,Go 语言会根据实际情况选择一个合适的值。

// 创建一个空的 Map
m := make(map[string]int)

// 创建一个初始容量为 10 的 Map
m := make(map[string]int, 10)

// 使用字面量创建 Map
m := map[string]int{
    "apple": 1,
    "banana": 2,
    "orange": 3,
}

// 获取键值对
v1 := m["apple"] // v1 = 1
v2, ok := m["pear"]  // 如果键不存在,ok 的值为 false,v2 的值为该类型的零值

// 修改键值对
m["apple"] = 5

// 获取 Map 的长度
len := len(m)

// 删除键值对
delete(m, "banana")

// 遍历 Map
for k, v := range m {
    fmt.Printf("key=%s, value=%d\n", k, v)
}

12. 接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

接口可以让我们将不同的类型绑定到一组公共的方法上,从而实现多态和灵活的设计。

Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。例如:

/* 定义接口 */
type interface_name interface {
    method_name1 [return_type]
    method_name2 [return_type]
}

/* 定义结构体 */
type struct_name struct {
    /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
    /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_name2() [return_type] {
    /* 方法实现*/
}

上面 struct_name 结构体类型实现了 interface_name 接口。

12.1. 一个完整例子

package main

import (
    "fmt"
)

// 接口
type Phone interface {
    call() // 方法,打电话
}

// 诺基亚手机
type NokiaPhone struct {
}

// 诺基亚手机实现了 Phone 接口
func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

// 苹果手机
type IPhone struct {
}

// 苹果手机实现了 Phone 接口
func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    // 声明一个 Phone 接口类型的变量
    var phone Phone

    // 创建一个诺基亚手机
    phone = new(NokiaPhone)
    phone.call() // 诺基亚手机打电话

    // 创建一个苹果手机
    phone = new(IPhone)
    phone.call() // 苹果手机打电话

}

12.2. new 函数

new是一个内置函数,用于为值类型(例如整数、浮点数、结构体等)分配内存,并返回指向新分配的零值的指针。

new函数的语法如下:

func new(type) *type

其中,type表示要分配内存的值的类型。

例如,以下代码使用new创建了一个person结构体的新实例:

// 定义一个结构体
type person struct {
    name string
    age  int
}

func main() {
    // 使用 new() 为 person 结构体分配内存
    p := new(person) 
    fmt.Printf("%T\n", p) 
    // 通过指针访问结构体字段并赋值
    p.name = "alice"
    p.age = 30
    // 显示结构体字段的值
    fmt.Println("name:", p.name)
    fmt.Println("age:", p.age)
}

13. 错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。error 类型是一个接口类型,它的定义:

type error interface {
    Error() string
}

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

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

14. 并发

并发是指程序同时执行多个任务的能力。Go 语言支持并发,通过 goroutines 和 channels 提供了一种简洁且高效的方式来实现并发。

14.1. goroutine

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。语法格式:

go 函数名( 参数列表 )

例如:

// 开启一个新的 goroutine
go f(x, y, z)

Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数,同一个程序中的所有 goroutine 共享同一个地址空间。

14.2. 通道(channel)

通道(channel)是用来传递数据的一个数据结构。

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。

使用 make 函数创建一个通道,使用 <- 操作符发送和接收数据。如果未指定方向,则为双向通道。

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据,并把值赋给 v

声明一个通道很简单,我们使用 chan 关键字即可,通道在使用前必须先创建:

ch := make(chan int)

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

14.2.1. 通道缓冲区

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

ch := make(chan int, 100)

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

14.2.2. 遍历通道与关闭通道

Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:

v, ok := <-ch

如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。例如:

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c) // 手动关闭通道
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
    // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
    // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
    // 会结束,从而在接收第 11 个数据的时候就阻塞了。
    for i := range c {
        fmt.Println(i)
    }
}

15. 附录

15.1. 容易犯的错误

15.1.1. 左边 { 花括号不能换行

package main

import "fmt"

func main()  
{  	// 错误,{ 不能在单独的行上
    fmt.Println("Hello, World!")
}

错误信息:

# command-line-arguments
.\tmp2.go:6:1: syntax error: unexpected semicolon or newline before {

15.2. 特殊语法

15.2.1. 下划线(_)

“_”(下划线)在 Go 语言中有多种用途,常见的用法包括:

  • 作为匿名变量:当你在使用多重赋值或函数返回多个值,但不需要使用其中的某个值时,可以使用“_”来忽略该值。例如:

func getValues() (int, string) {
    return 42, "answer"
}

_, value := getValues() 
// 这里使用 _ 忽略了第一个返回值(42),只获取了第二个返回值 "answer" 并赋值给 value
  • 在代码中作为占位符:有时候可能需要在代码中添加一些临时的或未使用的变量,使用“_”可以避免编译器产生未使用变量的错误。

  • 在导入包时,可以使用“_”来只执行包的初始化操作,而不使用包中的其他内容。例如:

import _ "some/package" 
// 只执行 "some/package" 包的初始化代码,而不直接使用包中的函数、变量等
  • 在结构体字段或接口方法中,如果某个字段或方法在当前上下文中不需要使用,也可以使用“_”作为字段名或方法名。

例如,在解析 JSON 数据到结构体时,如果某些字段不需要,可以使用“_”来忽略它们:

type Person struct {
    Name string
    Age  int
    _    string // 忽略该字段
}

总的来说,“_”的使用主要是为了提高代码的可读性、简洁性,或者处理一些不需要具体使用的值或情况。

我们一定不要当三等公民:等下班、等薪水、等退休。
0 不喜欢
说说我的看法 -
全部评论(
没有评论
关于
本网站属于个人的非赢利性网站,转载的文章遵循原作者的版权声明,如果原文没有版权声明,请来信告知:hxstrive@outlook.com
公众号