Re:从零开始的 Go 之旅-数据类型&常量&变量&输入输出

本文最后更新于 2025年1月22日 凌晨

部分内容和 C 语言的是一样的,所以就只记了不一样的

数据类型

字符类型

1
2
3
byte		等价 uint8 可以表达 ASCII 字符
rune 等价 int32 可以表达 Unicode 字符
string 字符串即字节序列,可以转化为 []byte 类型即字节切片

派生类型

1
2
3
4
5
6
7
8
数组		[1]int		长度为 1 的整型数组
切片 []float64 64位浮点数切片
映射表 map[string]int 键为字符串,值为整型的映射表
结构体 type Gopher struct{} Gopher 结构体
指针 *int
函数 type f func() 无参无返回值的函数
接口 type Gopher interface{} Gopher 接口
通道 chan int 整型通道

零值

零值又称为 zero value ,并不是指数字零 ,而是说一个类型的空值或默认值

1
2
3
4
5
6
数字类型		0
布尔类型 false
字符串类型 ""
数组 固定长度的对应类型的零值集合
结构体 内部字段都是零值的结构体
切片、映射表、函数、接口、指针、通道 nil

nil

nil 类似于 C 语言中的 null ,但有区别。nil 只是一些引用类型的零值,并且不属于任何类型,仅仅是一个变量。

常量

初始化

常量的声明需要 const 关键字,并且在初始化时必须赋值,不然无法通过编译

1
2
3
4
const name string = "butter" // 字面量
const msg = "hello butt3rf1y" // 字面量
const num = 114514 // 字面量
const numExpression = (1+2+3) / 2 % 100 + num // 常量表达式

批量声明常量可以用 () ,可以存在多个

1
2
3
4
5
6
7
8
9
const (
Count = 1
Name = "butter"
)

const (
Size = 14
Len = 51
)

在同一个常量组中,如果前一个常量赋值且后面没有赋值,那么默认后面的和前面赋值常量值相等,也就是说其值默认就是前面常量的值

1
2
3
4
5
6
7
const (
A = 1
B // 1
C // 1
D // 1
E // 1
)

iota

iota 是一个内置常量标识符,表示常量声明中无类型整数序数

1
const iota = 0

列子

1
2
3
4
5
6
7
const (
Num = iota // 0
Num1 // 1
Num2 // 2
Num3 // 3
Num4 // 4
)

也能这样写

1
2
3
4
5
6
7
const (
Num = iota*2 // 0
Num1 // 2
Num2 // 4
Num3 // 6
Num4 // 8
)

还可以

1
2
3
4
5
6
7
const (
Num = iota << 2*3 + 1 // 1
Num1 // 13 (iota << 2) * 3 + 1
Num2 // 25
Num3 = iota // 3
Num4 // 4
)

iota 的值本质上就是 iota 所在行相对于当前 const 分组的第一行的差值。而不同的 const 分组则相互不会影响。

1
2
3
4
5
6
7
8
9
const (
Num = iota<<2*3 + 1 // 1 第一行
Num2 = iota<<2*3 + 1 // 13 第二行
_ // 25 第三行
Num3 //37 第四行
Num4 = iota // 4 第五行
_ // 5 第六行
Num5 // 6 第七行
)

总结:iotaconst 块中从0开始,每次定义一个新常量时递增1,直到用新的 const 重置。如果常量定义中没有显式赋值,则会继承前一个常量的表达式。并且iota 的值在显式使用时才会更新。

枚举

在 go 中,通过自定义类型 + const + iota 来实现枚举

1
2
3
4
5
6
7
8
type Season uint8

const (
Spring Season = iota //0
Summer //1
Autumn //2
Winter //3
)

枚举出来的结果实际上就是数字,go 也不支持直接将其转换为字符串,但我们可以通过给自定义类型添加方法来返回其字符串表现形式,实现 Stringer 接口就行

1
2
3
4
5
6
7
8
9
10
11
12
13
func (s Season) String() string {
switch s {
case Spring:
return "spring"
case Summer:
return "summer"
case Autumn:
return "autumn"
case Winter:
return "winter"
}
return ""
}

变量

声明

go 中的声明是后置的,变量的声明会用到 var 关键字,格式为 var 变量名 类型名

1
2
3
var happy string
var intNUm int
var char byte

如果要多个同样类型的变量时,可以写在一起

1
var h1,h2,h3,h4 string

声明多个不同类型的变量时,可以使用 (),可以存在多个 ()

1
2
3
4
5
6
7
8
9
10
var (
name string
age int
address string
)

var (
school string
class int
)

注:变量如果只是声明而不赋值,那么变量存储的值就是对应类型的零值。

赋值

跟 C 语言规则差不多,但还可以分别赋值,比如说

1
2
3
var name string
var age int
name, age = "happy", 1

但是每次都要指定类型,还可以短变量初始化,省略 var 关键字和后置类型,编译器会自己判断

1
name := "happy"

虽然可以不用指定类型,但是在后续赋值时,类型必须保持一致,不然不能通过编译。还得注意的是,短变量初始化不能用 nil,因为 nil 不属于任何类型,编译器无法推断。

短变量还能批量初始化

1
name, age := "happy",1

短变量声明虽然无法对一个已存在的变量使用,但是在赋值旧变量的同时声明一个新的变量时可以使用

1
2
a := 1
a, b := 2, 2

在 go 中,所有在函数中的变量都必须要被使用,如果只是声明了变量,没有使用它就会报错,但仅适用于函数内的变量,对于函数外的包级变量无用。

匿名

_ 可以表示不需要某一个变量

1
Open(name string) (*File, error)

比如 os.Open 函数有两个返回值,我们只想要第一个,不想要第二个,可以按照下面这样写

1
file, _ := os.Open("readme.txt")

未使用的变量是无法通过编译的,当你不需要某一个变量时,就可以使用下划线 _ 代替。

交换

在 go 中,如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换,语法上看起来非常直观,例子如下

1
2
num1, num2 := 114, 514
num1, num2 = num2, num1

三个变量也是如此

1
2
num1, num2, num3 := 114, 514, 22
num1, num2, num3 = num3, num2, num1

go 里面有一个特别有意思的规则,进行多个变量赋值运算时,它的顺序是先计算值再赋值,不是从左到右计算,这就和其他语言大相径庭了。比如说下面的例子

1
2
a, b, c := 0, 1, 1
a, b, c = b, c, a+b

上面代码答案是

1
1 1 1

它会将 a, b, c 三个数的值分别计算好再赋给它们,就等同于下面这段代码

1
a, b, c = 1, 1, 0+1

当涉及到函数调用时,这个效果就更明显,有一个函数sum可以计算两个数字的返回值

1
2
3
func sum(a, b int) int {
return a + b
}

通过函数来进行两数相加

1
2
a, b, c := 0, 1, 1
a, b, c = b, c, sum(a, b)

结果没有变化,因为在计算sum函数返回值时,它传入的参数依旧是 0 和 1

1
1 1 1

所以代码应该写成这样

1
2
a, b = b, c
c = a + b

比较

变量之间的比较时它们的类型必须相同,go 语言中不存在隐式类型转换,像下面这样的代码编译是无法通过的

1
2
3
4
5
func main() {
var a uint64
var b int64
fmt.Println(a == b)
}

所以必须使用强制类型转换

1
2
3
4
5
func main() {
var a uint64
var b int64
fmt.Println(int64(a) == b)
}

可以使用 min 函数比较最小值

1
minVal := min(1, 2, -1, 1.2)

使用 max 函数比较最大值

1
maxVal := max(100, 22, -1, 1.12)

它们的参数支持所有的可比较类型,go 中的可比较类型有

  • 布尔
  • 数字
  • 字符串
  • 指针
  • 通道 (仅支持判断是否相等)
  • 元素是可比较类型的数组(切片不可比较)(仅支持判断是否相等)(仅支持相同长度的数组间的比较,因为数组长度也是类型的一部分,而不同类型不可比较)
  • 字段类型都是可比较类型的结构体(仅支持判断是否相等)

除此之外,还可以通过导入标准库 cmp 来判断,不过仅支持有序类型的参数,在 go 中内置的有序类型只有数字和字符串。

1
2
3
4
5
6
import "cmp"

func main() {
cmp.Compare(1, 2)
cmp.Less(1, 2)
}

代码块

函数内部可以通过花括号建立一个代码块,彼此之间的变量作用域是相互独立的,不受干扰,无法访问,但是会受到父块中的影响。

输入输出

文件描述符

1
2
3
4
5
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

os 包下有三个外暴露的文件描述符,其类型都是 *os.File ,分别是:

1
2
3
os.Stdin	标准输入
os.Stdout 标准输出
os.Stderr 标准错误

输入

read

像读文件一样(麻烦)

1
2
3
4
5
func main() {
var buf [1024]byte
n, _ := os.Stdin.Read(buf[:])
os.Stdout.Write(buf[:n])
}

fmt

fmt 包提供了几个函数

1
2
3
4
5
6
7
8
// 扫描从os.Stdin读入的文本,根据空格分隔,换行也被当作空格
func Scan(a ...any) (n int, err error)

// 与Scan类似,但是遇到换行停止扫描
func Scanln(a ...any) (n int, err error)

// 根据格式化的字符串扫描
func Scanf(format string, a ...any) (n int, err error)

读取两个数字

1
2
3
4
5
func main() {
var a, b int
fmt.Scanln(&a, &b)
fmt.Printf("%d + %d = %d\n", a, b, a+b)
}

读取固定长度的数组

1
2
3
4
5
6
7
8
9
10
11
func main() {
n := 10
s := make([]int, n)
for i := range n {
fmt.Scan(&s[i])
}
fmt.Println(s)
}

>> 1 2 3 4 5 6 7 8 9 10
>> [1 2 3 4 5 6 7 8 9 10]

bufio

有大量输入要读取的时建议使用 bufio.Reader

1
2
3
4
5
6
func main() {
reader := bufio.NewReader(os.Stdin)
var a, b int
fmt.Fscanln(reader, &a, &b)
fmt.Printf("%d + %d = %d\n", a, b, a+b)
}

scanner

bufio.Reader 类似,不过它按行读取

1
2
3
4
5
6
7
8
9
10
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
if line == "exit" {
break
}
fmt.Println("scan", line)
}
}

结果

1
2
3
4
5
6
7
1
scan 1
2
scan 2
3
scan 3
exit

输出

stdout

可以直接将字符串写入到标准输出中

1
2
3
4
5
6
7
package main

import "os"

func main() {
os.Stdout.WriteString("hello butt3rf1y!")
}

go 中两个内置的函数 printprintln,他们会将参数输出到标准错误中,仅做调试用,一般不推荐使用

fmt

fmt 包提供了 fmt.Println 函数,该函数默认会将参数输出到标准输出中。它的参数支持任意类型,如果类型实现了 String 接口也会调用 String 方法来获取其字符串表现形式,所以它输出的内容可读性比较高,适用于大部分情况,不过由于内部用到了反射,在性能敏感的场景不建议大量使用。

bufio

bufio 提供了可缓冲的输出方法,它会先将数据写入到内存中,积累到了一定阈值再输出到指定的 Writer 中,默认缓冲区大小是 4KB,在文件 IO,网络 IO 的时候使用比较好。

1
2
3
4
5
func main() {
writer := bufio.NewWriter(os.Stdout)
defer writer.Flush()
writer.WriteString("hello butt3rf1y!")
}

格式化

go 中的格式化输出功能基本上由 fmt.Printf 函数提供,跟 C 语言差不多。在 % 与格式化动词之间加上一个空格能达到分隔符的效果。


Re:从零开始的 Go 之旅-数据类型&常量&变量&输入输出
http://example.com/2025/01/22/数据类型/
作者
butt3rf1y
发布于
2025年1月22日
许可协议