基本数据类型Go 的基本数据类型分为 4 大类,相比于 Java 更简洁且明确:
Go 的基本数据类型分为 4 大类,相比于 Java 更简洁且明确:
| 类别 | 具体类型 | 说明 |
|---|---|---|
| 数值型 | int, int8, int16, int32, int64 |
Go 的 int 长度由平台决定(32 位系统为 4 字节,64 位为 8 字节),有符号整数(位数明确,如 int8 占 1 字节) |
uint, uint8, uint16, uint32, uint64, uintptr |
无符号整数(uintptr 用于指针运算) |
|
float32, float64 |
浮点数(默认 float64) |
|
complex64, complex128 |
复数(实部和虚部分别为 float32 或 float64,Java 无此类型) |
|
| 布尔型 | bool |
仅 true/false(不可用 0/1 替代) |
| 字符串 | string |
不可变的 UTF-8 字符序列 |
| 派生型 | byte(=uint8) |
1 字节数据 |
rune(=int32) |
Go 语言的字符(rune)使用 Unicode 来存储,而并不是字符本身,如果把 rune 传递给 fmt.Println 方法,会在控制台看到数字。虽然 Java 语言同样以 Unicode 保存字符(char),不过它会在控制台打印字符信息 |
Go 和 Java 同样都是 静态类型语言,要求在 编译期 确定所有变量的类型,且类型不可在运行时动态改变。Go 不允许任何隐式类型转换(如 int32 到 int64),但是在 Java 中允许基本类型隐式转换(如 int → long),除此之外,Go 语言会严格区分类型别名(如 int 与 int32 不兼容)。在 Go 语言中如果需要将不同类型的变量进行计算,需要进行类型转换:
package main
import "fmt"
func main() {
a := 1
b := 2.2
// 如果不类型转换则不能通过编译
fmt.Println(float64(a) * b)
}
“引用类型”
在 Go 语言中,严格来说并没有 “引用类型” 这一官方术语,但在 Go 语言社区中通常将 Slice(切片)、Map(映射)、Channel(通道) 称为 “引用语义类型”(或简称引用类型),因为它们的行为与传统的引用类型相似,在未被初始化时为 nil,并无特定的 “零值”。除了这三种类型之外,Go 的其他类型(如结构体、数组、基本类型等)都是 值类型。
Slice
Go 的 Slice 本质上是动态数组的抽象,基于底层数组实现自动扩容。它类似于 Java 中的 ArrayList,采用 var s []int 或 s := make([]int, 5) 声明,如下:
package main
import "fmt"
func slice() {
// 初始化到小为 0 的切片
s := make([]int, 0)
// 动态追加元素
s = append(s, 1, 2, 3, 4, 5)
fmt.Println(s)
// 子切片,左闭右开区间 sub = {2, 3}
sub := s[1:3]
fmt.Println(sub)
// 修改子切片值会影响到 s 原数组
sub[0] = 99
fmt.Println(s)
}
切片的底层数组并不能增长大小。如果数组没有足够的空间来保存新的元素,所有的元素会被拷贝至一个新的更大的数组,并且切片会被更新为引用这个新的数组。但是由于这些场景都发生在 append 函数内部,所发知道返回的切片和传入 append 函数的切片是否为相同的底层数组,所以如果保留了两个切片,那么这一点需要注意。
Map
Go 的 Map 本质上是无序键值对集合,基于哈希表实现。它的键必须支持 == 操作(如基本类型、结构体、指针),声明方式为 m := make(map[string]int) 或 m := map[string]int{"a": 1},它与 Java 中的 HashMap 类似,如下所示:
package main
import "fmt"
func learnMap() {
m := make(map[string]int)
m["a"] = 1
// 安全的读取
value, ok := m["a"]
if ok {
fmt.Println(value)
}
delete(m, "a")
}
Channel
Go 的 Channel 是用于 协程(goroutine,Go 语言中的并发任务类似 Java 中的线程)间通信 的管道,支持同步或异步数据传输。无缓冲区通道会阻塞发送 / 接收操作,直到另一端就绪。它的声明方式为 channel := make(chan string)(无缓冲)或 channel := make(chan string, 3)(有缓冲,缓冲区大小为 3),创建无缓存区的 channel 示例如下:
package main
import "fmt"
// 创建没有缓冲区的 channel,如果向其中写入值后而没有其他协程从中取值,
// 再向其写入值的操作则会被阻塞,也就是说“发送操作会阻塞发送 goroutine,直到另一个 goroutine 在同一 channel 上执行了接收操作”
// 反之亦然
func channel() {
channel1 := make(chan string)
channel2 := make(chan string)
// 启动一个协程很简单,即 go 关键字和要调用的函数
go abc(channel1)
go def(channel2)
// <- 标识符指出 channel 表示从协程中取值,输出一直都会是 adbecf
fmt.Print(<-channel1)
fmt.Print(<-channel2)
fmt.Print(<-channel1)
fmt.Print(<-channel2)
fmt.Print(<-channel1)
fmt.Println(<-channel2)
}
// <- 标识符指向 channel 表示向 channel 中发送值
func abc(channel chan string) {
channel <- "a"
channel <- "b"
channel <- "c"
}
func def(channel chan string) {
channel <- "d"
channel <- "e"
channel <- "f"
}
如果创建有缓冲的 channel,在我们的例子中,那么就可以实现写入协程不必等待 main 协程的接收操作了:
package main
import "fmt"
func channelNoBlocked() {
// 表示创建缓冲区大小为 3 的 channel,并且 channel 传递的类型为 string
channel1 := make(chan string, 3)
channel2 := make(chan string, 3)
go abc(channel1)
go def(channel2)
// 输出一直都会是 adbecf
fmt.Print(<-channel1)
fmt.Print(<-channel2)
fmt.Print(<-channel1)
fmt.Print(<-channel2)
fmt.Print(<-channel1)
fmt.Println(<-channel2)
}
在 Go 中创建上述三种引用类型的对象时,都使用了
make函数,它是专门用于初始化这三种引用类型的,如果不使用该函数,直接声明(如var m map[string]int)会得到nil值,而无法直接操作。它与 Java 中的new关键字操作有很大的区别,new关键字会为对象分配内存 并调用构造函数(初始化逻辑在构造函数中),而在 Go 的设计中是没有构造函数的,Go 语言除了这三种引用类型,均为值类型,直接声明即可,声明时便会直接分配内存并初始化为零值。
从失败中恢复
在 Go 语言中 没有传统 “异常” 概念,它不依赖 try/catch,而是通过 显式返回错误值 和 panic/recover 机制处理。它的错误(error)也是普通的数据,能够作为值传递。在多数方法中能看到如下类似的实现:
package main
func main() {
data, err := ReadFile("file.txt")
// 处理错误
if err != nil {
log.Fatal(err)
}
// ...
}
func ReadFile(path string) ([]byte, error) {
// 成功返回 data, nil
// 失败返回 nil, error
}
Go 语言使用 panic 来处理不可恢复的或程序无法继续运行的错误(如数组越界、空指针),这类似于 Java 语言中的 throw 异常,它会中断方法或函数的执行,向上抛出直到遇到 defer 和 recover() 函数的声明捕获或者程序崩溃:
// 初始化失败时触发 panic
func initDatabase() {
if !checkDatabaseConnection() {
panic("Database connection failed!")
}
}
// 通过 recover 捕获 panic
func main() {
// 延迟函数的执行
defer func() {
// 使用 recover() 函数尝试捕获异常
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
initDatabase()
// 正常逻辑...
}
defer 关键字 必须修饰的函数或方法,而且被这个关键字修饰的函数或方法 一旦注册 无论如何都会被执行(类似于 Java 中的 finally),但如果 defer 声明在函数尾部,但函数在运行到该 defer 语句之前就退出(例如中途 return 或 panic),则 defer 不会注册,也不会执行。所以该关键字在资源被初始化之后应该立即使用,而非像 Java 一样声明在方法的尾部。而且 defer 支持声明多个,但执行的顺序是逆序的。
revocer() 函数与 defer 关键字搭配使用,它会返回函数执行过程中抛出的 panic(未发生 panic 时会为 nil),可以帮助开发者恢复或提供有用的异常信息。
以下是在文件读取场景 Go 和 Java 语言在语法上的不同:
-
Go
func readFile() {
file, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 处理文件内容
}
-
Java
public void readFile() {
// try-with-resources
try (FileReader file = new FileReader("file.txt")) {
// 处理文件内容
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
问:我看到其他编程语言有
exception。panic和recover函数似乎以类似的方式工作。我可以把它们当作exception来使用吗?答:Go 语言维护者强烈建议不要这样做。甚至可以说,语言本身的设计不鼓励使用
panic和recover。在 2012 年的一次主题会议上,RobPike(Go 的创始人之一)把panic和recover描述为 “故意笨拙”。这意味着,在设计 Go 时,创作者们没有试图使panic和recover被容易或愉快地使用,因此它们会很少使用。这是 Go 设计者对exception的一个主要弱点的回应:它们可以使程序流程更加复杂。相反,Go 开发人员被鼓励以处理程序其他部分的方式处理错误:使用if和return语句,以及error值。当然,直接在函数中处理错误会使函数的代码变长,但这比根本不处理错误要好得多。(Go 的创始人发现,许多使用exception的开发人员只是抛出一个exception,之后并没有正确地处理它。)直接处理错误也使错误的处理方式一目了然,你不必查找程序的其他部分来查看错误处理代码。所以不要在 Go 中寻找等同于exception的东西。这个特性被故意省略了。对于习惯了使用exception的开发人员来说,可能需要一段时间的调整,但 Go 的维护者相信,这最终会使软件变得更好。
for 和 if
for
Go 语言的循环语法只有 for,没有 while 或 do-while,但可实现所有循环模式:
// 1. 经典三段式(类似 Java 的 for 循环)
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// 2. 类似 while 循环(条件在前)
sum := 0
for sum < 10 {
sum += 2
}
// 3. 无限循环(省略条件)
for {
fmt.Println("Infinite loop")
break // 需手动退出
}
// 4. 遍历集合,采用 range 关键字,index 和 value 分别表示索引和值
arr := []int{1, 2, 3}
for index, value := range arr {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
if
Go 语言的 if 语法相比于 Java 支持声明 + 条件的形式,并且强制要求大括号(即使是单行语句也必须使用 {}):
// 支持简短声明(声明 + 条件)
if num := 10; num > 5 {
fmt.Println("num is greater than 5")
}
// 简单判断
if num > 5 {
fmt.Println("num is greater than 5")
}
更多推荐


所有评论(0)