Hello World
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
包
每个 Go 程序都由包构成。
程序从 main 包开始运行。
本程序通过导入路径 "fmt" 和 "math/rand" 来使用这两个包。
按照约定,包名与导入路径的最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。
“导入路径”和“包名”的关系。
- 导入路径 (Import Path):是你在
import语句中写的那个字符串,是包的“地址”。例如import "math/rand"。 - 包名 (Package Name):是你在代码中实际使用的那个名字,用来调用包里的函数。这个名字是由包的源文件第一行
package <包名>决定的。
约定就是:package 关键字后面声明的那个名字,应该和导入路径的最后一部分相同。
我们来看 "math/rand" 这个例子:
- 导入路径是
"math/rand"。 - 这个路径的最后一个元素是
rand。 - 因此,按照约定,
math/rand目录下的所有.go文件的第一行必须是package rand。 - 当你在自己的代码里使用这个包时,你用的也是这个包名,而不是整个路径。例如:
rand.Intn(100),而不是math/rand.Intn(100)。
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println("我最喜欢的数字是 ", rand.Intn(10))
}
用圆括号将导入的包分成一组,这是“分组”形式的导入语句。
当然你也可以编写多个导入语句,例如:
import "fmt"
import "math"
不过使用分组导入语句要更好。
public 和 private 的规则
在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。例如,Pizza 就是个已导出名,Pi也同样,它导出自 math 包。
pizza 和 pi 并未以大写字母开头,所以它们是未导出的。
在导入一个包时,你只能引用其中已导出的名字。 任何「未导出」的名字在该包外均无法访问。
核心思想:通过首字母大小写来决定可见性
- 名字以大写字母开头 (e.g.,
MyVariable,MyFunction) = 已导出 (Exported) = 公开的 (Public)- 可以在任何地方被访问,尤其是在导入了该包的其他包中。
- 名字以小写字母开头 (e.g.,
myVariable,myFunction) = 未导出 (Unexported) = 私有的 (Private)- 只能在它自己所在的包内部被访问。其他包(即便是导入了它的包)也无法访问。
| Go 语言 | C++ 语言 | 含义 |
|---|---|---|
Name (首字母大写) |
public: |
公开的:包外部可以访问。 |
name (首字母小写) |
private: |
私有的:只能在包内部访问。 |
| (无) | protected: |
(Go语言没有 protected 的概念) |
函数
接受参数
函数可接受零个或多个参数。当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。
注意类型在变量名的 后面。
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
任意数量的返回值
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)
}
- 签名即文档 (Signature is Documentation):看到函数签名
func (float64, error),你立刻就知道这个函数可能会失败,并且会返回一个错误信息。这比C++的异常机制要明确得多,因为异常在函数签名中是不可见的。 - 错误是普通的值 (Errors are values):在Go中,错误(
error类型)就是一个普通的值,你可以像处理任何其他值一样处理它(传递、存储、判断),而不是像异常那样需要特殊的try-catch语法块。这让错误处理的逻辑变得非常清晰和可控。 - 鼓励显式处理错误:
result, err := ...这种写法,迫使程序员必须处理err这个变量(否则编译器会报错“变量已声明但未使用”),从而大大减少了因忘记检查错误而导致的程序bug。
带名字的返回值
Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。
返回值的命名应当能反应其含义,它可以作为文档使用。
没有参数的 return 语句会直接返回已命名的返回值,也就是「裸」返回值。
裸返回语句应当仅用在下面这样的短函数中。在长的函数中它们会影响代码的可读性。
- 标准(无名)的返回值
我们先写一个标准的函数,它接受一个整数 sum,然后把它拆分成两部分返回。
package main
import "fmt"
// 标准函数:返回值只有类型 (int, int),没有名字
func split(sum int) (int, int) {
x := sum * 4 / 9
y := sum - x
// 必须显式地返回你想返回的变量
return x, y
}
func main() {
a, b := split(17)
fmt.Println(a, b) // 输出: 7 10
}
这是我们之前讨论过的标准多返回值函数,非常清晰。
- 带名字的返回值
现在,我们用“带名字的返回值”来重写上面完全相同的函数。
package main
import "fmt"
// 带名字的返回值:在返回值类型前加上名字 (x int, y int)
func splitNamed(sum int) (x, y int) {
// 这里的 x 和 y 就是我们刚刚在函数签名里命名的返回值变量
x = sum * 4 / 9
y = sum - x
// 使用「裸」返回语句
return
}
func main() {
a, b := splitNamed(17)
fmt.Println(a, b) // 输出: 7 10
}
a. “Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。”
- 意思就是:当你写下
func splitNamed(sum int) (x, y int)时,Go编译器在函数内部帮你做了两件事:所以,你一进入splitNamed函数,就可以直接使用x和y这两个变量了,不需要再用:=或var来声明它们。- 它自动声明了两个变量,一个叫
x,一个叫y,类型都是int。 - 它将这两个变量的初始值设为它们类型的“零值”(对于
int就是0)。
- 它自动声明了两个变量,一个叫
b. “返回值的命名应当能反应其含义,它可以作为文档使用。”
- 意思就是:命名可以增加代码的可读性。对比一下两个函数签名:第二个签名更清晰地告诉了阅读代码的人:这个函数返回的两个
int分别被作者命名为了x和y,这给了调用者一个关于返回值含义的提示。如果名字起得更有意义,比如(quotient int, remainder int),那么文档效果会更好。func split(sum int) (int, int)func splitNamed(sum int) (x, y int)
c. “没有参数的 return 语句会直接返回已命名的返回值,也就是「裸」返回值。”
- 意思就是:在
splitNamed函数的末尾,我们只写了一个return,后面没有跟任何变量。 - 这个“裸”的
return是一个快捷方式,它等价于return x, y。也就是说,它会自动去寻找你在函数签名里命名的那些返回值变量,并把它们的当前值返回。
d. “裸返回语句应当仅用在下面这样的短函数中。在长的函数中它们会影响代码的可读性。”
- 这是一个非常非常重要的编程建议。
- 为什么会影响可读性? 想象一个很长的函数(比如50行),它有命名的返回值
(result int, err error)。在函数的第10行,你可能写了result = 1;在第25行,你又写了result = 2;在第40行,你写了err = errors.New(...)。当读者看到函数末尾只有一个孤零零的return时,他必须把整个函数从头到尾再读一遍,才能确定result和err的最终值到底是什么。 - 显式返回更清晰:在长函数中,如果在最后明确地写
return result, err,代码的意图就会一目了然,大大提高了可维护性。
变量
var声明
var 语句用于声明一系列变量。和函数的参数列表一样,类型在最后。其可以出现在包或函数的层级。
package main
import "fmt"
var c, python, java bool
func main() {
var i int
fmt.Println(i, c, python, java)
}
如果初始化时提供了值,Go可以自动推导类型,此时可以省略类型声明。
// 编译器看到 10,自动推断 i 的类型是 int
var i = 10
当你想一次性声明多个相关的变量时,可以使用 var 配合圆括号 ()。
var (
isActive bool = false
name string = "admin"
retry int = 3
)
短变量声明
在函数中,短赋值语句 := 可在隐式确定类型的 var 声明中使用。
函数外的每个语句都 必须 以关键字开始(var、func 等),因此 := 结构不能在函数外使用。
基本类型
1. 布尔类型 (Boolean)
- Go:
bool - 值:
true和false。
与C++的关键区别: Go的bool类型和整型不能相互转换。在C++中,0可以当作false,非0可以当作true,但在Go中这是不允许的,会导致编译错误。
// 以下代码在 Go 中是错误的
// var i int = 1
// if i { ... }
// 必须写成明确的比较
var i int = 1
if i == 1 { ... }
这个设计大大增强了代码的类型安全性和清晰度。
2. 字符串类型 (String)
- Go:
string - 与C++的关键区别:
- 内置类型: 在Go中,
string是像int和bool一样的基础类型,而不是像C++的std::string那样的库类型。
- 内置类型: 在Go中,
不可变性 (Immutability): 这是最重要的区别。Go的字符串一旦被创建,其内容就不能被修改。任何对字符串的“修改”操作(如拼接)实际上都会创建一个新的字符串对象。
var s string = "hello"
// s[0] = 'H' // 这在Go中是编译错误!
s = "world" // 这是合法的,因为你是让 s 指向一个“新”的字符串,而不是修改原来的 "hello"
s = s + ", Gopher!" // 拼接操作会创建一个全新的字符串,然后让 s 指向它
C++的std::string则是可变的。这种不可变性设计使得Go在并发场景下传递字符串变得非常安全,因为你不需要担心它在别处被意外修改。
3. 整型 (Integer)
- Go:
int,uint,int8...int64,uint8...uint64,uintptr - 与C++的相似之处: C++也有类似
int8_t,int16_t等固定宽度的整数(在<cstdint>头文件中),所以这个概念对你来说很熟悉。它们都用于需要精确控制内存布局或与外部系统交互的场景。 int,uint,uintptr的平台依赖性:- 这一点与C++中的
long或者size_t类似。它们的大小取决于你的目标编译平台是32位还是64位。 - 最佳实践: 当需要一个整数值时应使用
int类型。这是因为int被设计为在目标平台上处理效率最高的整数类型,非常适合用作数组/切片的索引、循环计数器等。只有当你明确需要一个特定大小(比如要写入二进制文件)或者需要处理超过int范围的无符号数时,才去选择int64,uint32等。
- 这一点与C++中的
4. 别名与特殊用途类型 (byte 和 rune)
这两个是Go为了增强代码可读性而设定的别名,非常重要。
byte//uint8的别名- 用途: 它明确地告诉阅读代码的人,我们在这里处理的是原始的字节数据,而不是一个小的数字。例如,在读取文件、处理网络数据流时,你会用
[]byte。 - C++类比: 概念上完全等同于
unsigned char或者uint8_t,它们都代表一个字节。
- 用途: 它明确地告诉阅读代码的人,我们在这里处理的是原始的字节数据,而不是一个小的数字。例如,在读取文件、处理网络数据流时,你会用
rune//int32的别名- 用途: 这是Go语言处理Unicode的基石。一个
rune代表一个Unicode码点 (Code Point)。简单来说,它就代表一个字符,无论这个字符在UTF-8编码下占多少个字节。 - C++类比:
rune在概念上等同于C++11引入的char32_t,都用于表示一个Unicode码点。Go将其内置并作为处理字符串的核心部分,使得国际化文本处理更加简单和安全。
- 用途: 这是Go语言处理Unicode的基石。一个
为什么需要它?: Go的string内部是UTF-8编码的字节序列。在UTF-8中,一个英文字母(如'a')占1个字节,而一个中文字符(如'好')占3个字节。如果你按字节遍历字符串,就会把一个汉字拆开。rune就是为了解决这个问题。
s := "你好"
fmt.Println(len(s)) // 输出 6, 因为 "你好" 在UTF-8中占 6 个字节
// 使用 for...range 遍历字符串时,Go会自动按 rune 进行解码
for _, r := range s {
fmt.Printf("%c ", r) // 输出: 你 好
}
5. 浮点型与复数 (Floating-Point & Complex)
- Go:
float32,float64和complex64,complex128 - 与C++的类比:
float32对应 C++ 的float。float64对应 C++ 的double。(在Go中,像3.14这样的浮点数字面量,默认类型是float64)。complex64(实部虚部都是float32) 对应std::complex<float>。complex128(实部虚部都是float64) 对应std::complex<double>。 这部分的概念和用法与C++几乎完全一致。
零值
没有明确初始化的变量声明会被赋予对应类型的 零值。
零值是:
- 数值类型为
0, - 布尔类型为
false, - 字符串为
""(空字符串)。
类型转换
Go语言的设计者认为,任何可能导致数据丢失或改变数值含义的操作,都应该由程序员明确地写出来。编译器不会替你做任何“猜测”。所以Go中没有任何的隐式转换。
语法: T(v)
Go的类型转换语法非常统一且简单:目标类型(值)。
T代表目标类型 (e.g.,float64,uint,int32)。v代表你要转换的值 (e.g.,i,f,42)。
这种语法形式取代了C语言风格的 (T)v。
类型推断
在声明一个变量而不指定其类型时(即使用不带类型的 := 语法 var = 表达式语法),变量的类型会通过右值推断出来。
当声明的右值确定了类型时,新变量的类型与其相同,不过当右边包含未指明类型的数值常量时,新变量的类型就可能是 int、float64 或 complex128了,这取决于常量的精度。
在Go中,像 42, 3.142 这样的常量字面量在代码中是“无类型的”。你可以把它们想象成存在于一个理想的数学世界里的、精度极高的“数字”,它们还没有被强制转换成计算机内存中某个特定大小的类型(比如 int32 或 float64)。只有当这个“无类型常量”被用于需要一个具体类型的上下文时(比如赋值给一个用 := 声明的变量),Go才会根据这个常量的形式,为新变量选择一个默认类型。
当你在Go代码中写下一个数字(如 100、3.141592653589793)时,编译器并不会立即把它“塞进”一个int32或float64这样有大小限制的类型里。相反,编译器在内部用一种精度非常高的数据结构来表示这个数字,把它当作一个纯粹的、理想的数学值。
这个高精度的值非常灵活,像“变色龙”一样。当需要它的时候,它会根据上下文(变量类型、函数签名等)“变成”那个类型,前提是它的值必须在目标类型的合法范围内。如果超出范围,编译器就会报错。也就是只有这个量被装进一个口袋的时候,才会真正检查是否合适。
因为这些常量是“无类型的”,所以它们可以灵活地用于任何能够容纳它的值的上下文中,而不需要进行显式类型转换。
// const 关键字声明的常量,如果没有指定类型,也是“无类型”的
const BigNumber = 1000
const SmallNumber = 1
func main() {
// BigNumber 是一个无类型的整数常量
var i64 int64 = BigNumber // 合法!BigNumber可以被用作int64
var f64 float64 = BigNumber // 合法!BigNumber也可以被用作float64
var i8 int8 = SmallNumber // 合法!SmallNumber可以被用作int8
// 现在我们声明一个“有类型”的常量
const TypedInt int = 1000
// var another_i8 int8 = TypedInt // 编译错误!
// 上面这行会失败,因为 TypedInt 的类型是 int,和 int8 是不同类型,
// Go不允许在不同类型间隐式赋值,即使值1000可以被int8容纳(这里假设int是32位)。
// 实际上1000也超出了int8的范围,但即使是100也同样会报错。
}
常量
常量的声明与变量类似,只不过使用 const 关键字。
常量可以是字符、字符串、布尔值或数值。
常量不能用 := 语法声明。
iota:Go特有的常量生成器
在 const 声明块中,有一个非常有用的特殊常量 iota,它可以被看作是常量计数器。
iota在每个const块的开始时被重置为0。- 在块中,每新增一行常量声明,
iota的值就会自动递增1。
这使得 iota 成为创建枚举 (enum) 或递增序列的最佳工具。
示例1:创建简单的枚举
const (
Sunday = iota // iota = 0, Sunday = 0
Monday // 自动使用上一行的表达式,iota = 1, Monday = 1
Tuesday // iota = 2, Tuesday = 2
Wednesday // iota = 3, Wednesday = 3
// ...
)
C++类比: 这和C++的 enum { Sunday, Monday, ... }; 效果几乎完全一样。
示例2:创建位掩码 (Bitmasks)
iota 强大的地方在于它可以参与表达式运算。
Go
const (
FlagRead = 1 << iota // 1 << 0 (值为 1)
FlagWrite = 1 << iota // 1 << 1 (值为 2)
FlagExecute = 1 << iota // 1 << 2 (值为 4)
)