Golang
Go,也称为 Golang,是一种开源的编程语言,由 Google 开发。Go 是一种静态类型、编译型、并发型的语言,它被设计为一种简单、快速、可靠和高效的语言。Go 语言的语法类似于 C 语言,但也借鉴了其他语言的一些特性,如 Python 和 Java。
Go 语言的设计目标是提供一种简单、易于学习和使用的语言,同时具有高效的执行速度和并发处理能力。Go 语言的主要特点包括:
1. 并发支持:Go 语言内置了并发支持,可以轻松地编写高并发的程序,而不需要额外的库或框架。
2. 内存管理:Go 语言有自己的垃圾收集器,可以自动管理内存,避免内存泄漏和悬垂指针等问题。
3. 快速编译:Go 语言的编译速度非常快,可以在几秒钟内编译大部分代码。
4. 简单易学:Go 语言的语法简单,易于学习和使用,同时也提供了足够的功能和扩展性。
5. 跨平台支持:Go 语言可以在各种操作系统和硬件平台上运行,包括 Windows、Linux、macOS、Android、iOS 等。
6. 开源:Go 语言是一个开源项目,可以自由下载、使用和修改。
Go 语言被广泛应用于网络编程、云计算、大数据处理、系统编程、区块链等领域。许多知名公司和组织,如 Google、Uber、Docker、Kubernetes、Cloudflare 等都在使用 Go 语言开发自己的项目和服务。
查看更多相关内容
如何在Go中使用“sync/atomic”包执行原子操作?
在Go语言中,`sync/atomic`包提供了低级的原子内存操作接口,这些接口对于同步算法的实现是很有用的,尤其是在无锁编程中。原子操作是指在多线程环境下,操作的执行不会被其他线程的活动打断。这种操作对于防止竞态条件非常必要。
下面我将介绍如何使用`sync/atomic`包来执行一些基本的原子操作,以及一个具体的例子来说明如何在实际中运用这些操作。
### 基本原子操作
`sync/atomic` 包提供了几种类型的原子操作,主要包括:
- **增加**(`Add`系列函数,如`AddInt32`, `AddInt64`等)
- **比较并交换**(`CompareAndSwap`系列函数,如`CompareAndSwapInt32`, `CompareAndSwapPointer`等)
- **载入**(`Load`系列函数,如`LoadInt32`, `LoadPointer`等)
- **存储**(`Store`系列函数,如`StoreInt32`, `StorePointer`等)
- **交换**(`Swap`系列函数,如`SwapInt32`, `SwapPointer`等)
### 例子:原子计数器
假设我们需要在多个goroutine中共享一个计数器,那么就需要确保对计数器的访问是线程安全的。我们可以使用`sync/atomic`包中的`AddInt64`函数来实现一个线程安全的原子计数器。
```go
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
var counter int64
var wg sync.WaitGroup
// 模拟10个goroutine同时更新计数器
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for c := 0; c < 100; c++ {
atomic.AddInt64(&counter, 1)
time.Sleep(time.Millisecond)
}
wg.Done()
}()
}
wg.Wait()
fmt.Printf("Final counter value: %d\n", counter)
}
```
在这个例子中,我们创建了10个goroutine,每个都对计数器增加100次,每次增加后等待1毫秒。我们使用`AddInt64`来保证每次增加操作的原子性。这样做可以保证无论在什么情况下,最终的计数器值都是正确的,即1000。
### 结论
使用`sync/atomic`包可以有效地实现原子操作,增强程序在并发环境下的稳定性和准确性。在任何需要确保数据在多个goroutine间同步的场景下,原子操作都是一个值得考虑的解决方案。
11月29日 09:45
Go中上下文包的作用是什么?
在Go语言中,`context` 包的主要目的是为程序中运行的goroutine提供一个统一的方式来传递取消信号、超时时间、截止日期以及其他请求范围的值。这对于控制和管理那些需要长时间运行并且可能需要被优雅终止的操作非常重要。
### 主要功能
1. **取消信号**:
`context` 包可以被用来发送取消信号给与之相关的goroutine。这在需要中断例如网络调用、数据库查询或者其他可能长时间运行的任务时非常有用。
**例子**:
假设我们有一个网络服务,当接收到某个特定API调用时,它会启动一个长时间运行的数据处理操作。如果用户在操作完成之前取消了请求,我们可以使用context来取消所有相关的goroutine,防止资源浪费。
2. **超时与截止**:
使用`context`,开发者可以设置超时或者截止时间,一旦超过指定时间,与之相关的操作就会被自动取消。
**例子**:
例如,我们可以为数据库查询设置一个30秒的超时时间。如果查询超过了这个时间还没有完成,系统将自动终止查询,并且返回超时错误。
3. **值传递**:
`context` 还提供了一种安全的方式来传递请求范围内的值。这些值可以跨API边界和goroutine安全地传递。
**例子**:
在一个web服务中,可以通过context传递请求的唯一ID,这样在处理请求的整个链路中,从日志记录到错误处理都可以访问这个ID,方便跟踪和调试。
### 使用场景
- **HTTP请求处理**:
Go的`net/http`包使用context来管理每一个请求。每个请求都有一个与之关联的context,这个context会在请求结束时自动取消。
- **数据库和网络操作**:
数据库操作和外部API调用经常使用context来实现超时控制和取消操作,保证服务的健壷性和响应性。
总结来说,`context` 包在Go中是一个非常重要的工具,用于在并发操作中实现超时控制、任务取消以及值的安全传递,帮助开发者构建可维护和健壮的系统。
阅读 7 · 8月24日 15:05
Go中浅拷贝和深拷贝有什么区别?
在Go语言中,浅拷贝和深拷贝是两种不同的数据复制方式,它们在处理复杂数据结构时的表现和影响也有很大的不同。
### 浅拷贝(Shallow Copy)
浅拷贝仅仅复制数据结构的顶层元素,对于其中的引用类型(如指针、切片、映射、接口等),浅拷贝不会复制它们所指向的底层数据,而是仅仅复制引用。这意味着,如果原始数据结构中的引用类型的元素被修改,那么所有的浅拷贝副本中相应的数据也会发生变化,因为它们指向的是同一块内存地址。
**例子:**
```go
type Person struct {
Name string
Age int
Tags []string
}
func main() {
p1 := Person{Name: "John", Age: 30, Tags: []string{"friendly", "team-player"}}
p2 := p1 // 浅拷贝
p2.Name = "Mike"
p2.Tags[0] = "aggressive"
fmt.Println(p1) // 输出: {John 30 [aggressive team-player]}
fmt.Println(p2) // 输出: {Mike 30 [aggressive team-player]}
}
```
在这个例子中,尽管我们改变了`p2`的`Name`字段,`p1`的`Name`并没有变化;但是当我们修改`p2`的`Tags`切片时,`p1`中相应的`Tags`也发生了变化,因为切片是引用类型。
### 深拷贝(Deep Copy)
深拷贝不仅复制数据结构的顶层元素,还会递归地复制所有引用类型的底层数据。这意味着,复制过程中会创建一份完全独立的数据副本,原始数据的任何修改都不会影响到深拷贝的结果。
**例子:**
```go
func DeepCopy(src, dst interface{}) {
marshall, _ := json.Marshal(src)
json.Unmarshal(marshall, dst)
}
func main() {
p1 := Person{Name: "John", Age: 30, Tags: []string{"friendly", "team-player"}}
var p2 Person
DeepCopy(p1, &p2)
p2.Name = "Mike"
p2.Tags[0] = "aggressive"
fmt.Println(p1) // 输出: {John 30 [friendly team-player]}
fmt.Println(p2) // 输出: {Mike 30 [aggressive team-player]}
}
```
在这个例子中,使用了JSON序列化和反序列化来实现深拷贝。可以看到,`p2`的修改完全不影响`p1`,因为它们是完全独立的两份数据。
### 总结
选择浅拷贝还是深拷贝,取决于你的具体需求。如果你需要完全独立的数据副本,应该使用深拷贝。如果你只需要复制数据结构的表层数据,并且对数据共享的影响有所了解,则可以使用浅拷贝。
阅读 7 · 8月24日 15:04
如何在Go中使用“net”包来实现网络协议?
在Go语言中,`net` 包提供了广泛的网络通信功能,包括TCP/IP、UDP协议、域名解析等。下面我将通过几个步骤和示例,详细说明如何在 Go 中使用 `net` 包来实现网络协议。
#### 1. 创建TCP服务器
要实现一个TCP服务器,您需要使用 `net.Listen` 函数来监听一个端口,然后使用 `Accept` 方法等待客户端的连接请求。
```go
package main
import (
"fmt"
"net"
"os"
)
func main() {
// 监听8080端口
ln, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Fprintf(os.Stderr, "错误: %v\n", err)
return
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
fmt.Fprintf(os.Stderr, "连接错误: %v\n", err)
continue
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
fmt.Fprintf(os.Stderr, "读取错误: %v\n", err)
return
}
fmt.Printf("收到消息: %s\n", string(buffer[:n]))
conn.Write([]byte("收到消息\n"))
}
```
#### 2. 创建TCP客户端
创建TCP客户端比较简单,使用 `net.Dial` 连接到服务器。
```go
package main
import (
"fmt"
"net"
)
func main() {
// 连接到服务器
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
fmt.Println("错误:", err)
return
}
defer conn.Close()
// 发送数据
_, err = conn.Write([]byte("你好,服务器!"))
if err != nil {
fmt.Println("写入错误:", err)
return
}
// 接收数据
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
fmt.Println("读取错误:", err)
return
}
fmt.Println("从服务器收到:", string(buffer[:n]))
}
```
#### 3. 使用UDP协议
UDP不同于TCP,它是无连接的。下面是基于UDP的服务器和客户端的简单例子。
**UDP服务器:**
```go
package main
import (
"fmt"
"net"
)
func main() {
addr := net.UDPAddr{
Port: 8081,
IP: net.ParseIP("127.0.0.1"),
}
conn, err := net.ListenUDP("udp", &addr)
if err != nil {
fmt.Println("错误:", err)
return
}
defer conn.Close()
buffer := make([]byte, 1024)
for {
n, remoteAddr, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Println("读取错误:", err)
continue
}
fmt.Printf("收到来自 %v 的消息: %s\n", remoteAddr, string(buffer[:n]))
conn.WriteToUDP([]byte("消息已收到\n"), remoteAddr)
}
}
```
**UDP客户端:**
```go
package main
import (
"fmt"
"net"
)
func main() {
serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8081")
if err != nil {
fmt.Println("错误:", err)
return
}
conn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil {
fmt.Println("连接错误:", err)
return
}
defer conn.Close()
_, err = conn.Write([]byte("你好,UDP服务器!"))
if err != nil {
fmt.Println("写入错误:", err)
return
}
buffer := make([]byte, 1024)
n, _, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Println("读取错误:", err)
return
}
fmt.Println("从服务器收到:", string(buffer[:n]))
}
```
这些示例展示了如何在Go中使用 `net` 包创建TCP和UDP的服务器与客户端。通过这些基本的构建块,您可以构建更复杂的网络通信应用。
阅读 6 · 8月24日 15:04
在Go中如何处理文件I/O?
在Go语言中,处理文件I/O主要涉及到几个核心的包:`os`、`bufio` 和 `ioutil`。下面我将分别介绍这些包的主要用法,并给出一些实际的代码示例。
### 1. 使用 `os` 包
`os` 包提供了基本的文件操作功能,如打开、读取、写入和关闭文件。
**打开文件:**
```go
file, err := os.Open("filename.txt")
if err != nil {
// 错误处理
log.Fatal(err)
}
defer file.Close()
```
这里,`os.Open` 用于读取操作。如果是需要写入操作,可以用 `os.Create` 或 `os.OpenFile`。
**读取文件:**
```go
data := make([]byte, 100) // 创建缓冲区
count, err := file.Read(data)
if err != nil {
// 错误处理
log.Fatal(err)
}
fmt.Println("读取的字节数: ", count)
fmt.Println("数据内容: ", string(data))
```
**写入文件:**
```go
data := []byte("hello, world\n")
count, err := file.Write(data)
if err != nil {
// 错误处理
log.Fatal(err)
}
fmt.Println("写入的字节数: ", count)
```
### 2. 使用 `bufio` 包
`bufio` 包提供了缓冲读写功能,它封装了 `os.File` 对象,使得读写操作更加高效。
**创建一个缓冲读取器:**
```go
reader := bufio.NewReader(file)
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
// 错误处理
log.Fatal(err)
}
fmt.Println("读取的数据: ", line)
```
**创建一个缓冲写入器:**
```go
writer := bufio.NewWriter(file)
bytesWritten, err := writer.WriteString("Hello, Gophers!\n")
if err != nil {
log.Fatal(err)
}
writer.Flush() // 确保所有缓冲的数据都已经写入到底层的 io.Writer 中
fmt.Println("写入的字节数: ", bytesWritten)
```
### 3. 使用 `ioutil` 包
虽然从 Go 1.16 开始,`ioutil` 包中的大多数功能已经被并入 `os` 或 `io` 包,但它在早期版本中常用于简化文件读写操作。
**读取整个文件:**
```go
data, err := ioutil.ReadFile("filename.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println("文件内容: ", string(data))
```
**写入整个文件:**
```go
err := ioutil.WriteFile("filename.txt", data, 0644)
if err != nil {
log.Fatal(err)
}
```
### 总结
以上就是在Go语言中处理文件I/O的几种主要方式。通过结合 `os`、`bufio` 以及(在旧版本Go中)`ioutil` 包,您可以灵活地执行各种文件操作。在日常开发中,选择合适的方法取决于具体的场景需求,如是否需要高效的缓冲读写,或是简单的一次性读写等。
阅读 7 · 8月24日 15:04
如何使用“errors”包在Go中创建和操作错误?
在 Go 语言中,`errors` 包是用于创建和操作错误的一个非常基础和有用的包。它提供了基本但足够强大的工具来处理错误。以下是一些使用 `errors` 包来创建和操作错误的步骤和例子:
### 1. 创建一个简单的错误
要创建一个新的错误,我们可以使用 `errors.New` 函数。这个函数接受一个字符串参数作为错误信息,并返回一个错误对象。
```go
import (
"errors"
"fmt"
)
func main() {
err := errors.New("这是一个错误信息")
if err != nil {
fmt.Println(err)
}
}
```
### 2. 使用 `fmt.Errorf` 创建带有格式化的错误
如果你想在错误消息中包含变量或更复杂的格式,可以使用 `fmt.Errorf`。它工作方式类似于 `fmt.Sprintf`,但返回一个错误对象。
```go
func validateAge(age int) error {
if age < 18 {
return fmt.Errorf("年龄 %d 小于 18,不符合要求", age)
}
return nil
}
func main() {
err := validateAge(16)
if err != nil {
fmt.Println(err)
}
}
```
### 3. 错误的包装(Wrapping)
Go 1.13 引入了错误包装的概念,这使得你可以“包装”一个错误,以添加更多的上下文信息,同时还可以保留原始错误的类型和内容。你可以使用 `%w` 占位符来实现这一点。
```go
func readFile(filename string) error {
_, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("读取文件 '%s' 时出错: %w", filename, err)
}
return nil
}
func main() {
err := readFile("不存在的文件.txt")
if err != nil {
fmt.Println(err)
// 使用 errors.Unwrap 解包原始错误
if originalErr := errors.Unwrap(err); originalErr != nil {
fmt.Println("原始错误:", originalErr)
}
}
}
```
### 4. 检查特定的错误
有时候,我们需要基于错误类型或错误内容作出不同的处理决策。我们可以使用 `errors.Is` 和 `errors.As` 函数来检查错误。
```go
var ErrNetworkTimeout = errors.New("网络超时")
func performRequest() error {
// 模拟网络请求错误
return fmt.Errorf("请求错误: %w", ErrNetworkTimeout)
}
func main() {
err := performRequest()
if errors.Is(err, ErrNetworkTimeout) {
fmt.Println("捕获到网络超时,可能需要重试")
} else {
fmt.Println("发生其他错误")
}
}
```
在面试中,使用具体的例子来说明你如何使用一个工具或方法总是一个加分项。通过上述步骤和示例,我展示了如何在 Go 程序中创建和操作错误,这是开发中一个非常重要的部分,确保程序的健壯性和可维护性。
阅读 7 · 8月24日 15:04
如何在 Go 中使用命令行参数?
在 Go 语言中,使用命令行参数可以通过 `os` 包中的 `Args` 变量来实现。`os.Args` 是一个字符串切片(slice),包含了启动程序时传递给程序的所有命令行参数。`os.Args[0]` 是程序的名称,`os.Args[1:]` 是传递给程序的参数。
以下是使用命令行参数的一个基本示例:
```go
package main
import (
"fmt"
"os"
)
func main() {
// 检查命令行参数的个数
if len(os.Args) < 2 {
fmt.Println("请输入至少一个参数!")
return
}
// 遍历从命令行接收的所有参数
for index, arg := range os.Args[1:] {
fmt.Printf("参数 %d: %s\n", index+1, arg)
}
}
```
假设我们的程序叫做 `example`,我们可以在命令行中这样运行它:
```
$ go run example.go arg1 arg2 arg3
```
程序将输出:
```
参数 1: arg1
参数 2: arg2
参数 3: arg3
```
此程序首先检查是否有足够的命令行参数传递进来(至少需要一个参数)。然后,它通过一个循环遍历 `os.Args[1:]` 切片,这部分切片排除了程序名称,仅包含传递给程序的参数。
这个例子演示了如何接收和处理命令行参数,这在很多命令行应用程序中是非常有用的。例如,你可以基于传递的参数来改变程序的行为,处理文件输入输出,或者设置程序运行的配置等。
阅读 9 · 8月23日 00:12
如何处理 golang 中的 goroutines 并获得响应
在Go语言中,goroutines 是一种非常轻量级的线程,用于并发执行任务。处理 goroutines 并获取其响应可以通过多种方式实现,最常见的方法是使用通道(channels)和 sync 包中的工具,如 WaitGroup。下面我将详细介绍这两种方法,包括具体的例子。
### 1. 使用通道(Channels)
通道是用来在不同的 goroutines 之间安全地传递数据的。你可以使用通道来获取 goroutine 的执行结果。
**例子:**
假设我们需要计算多个数的平方,并获取结果。
```go
package main
import (
"fmt"
"time"
)
func square(number int, ch chan int) {
// 计算平方
result := number * number
// 模拟一些耗时
time.Sleep(time.Second)
// 将结果发送到通道中
ch <- result
}
func main() {
ch := make(chan int)
numbers := []int{2, 4, 6, 8}
for _, number := range numbers {
go square(number, ch)
}
for range numbers {
fmt.Println(<-ch)
}
}
```
在这个例子中,我们创建了一个名为 `square` 的函数,它接受一个整数和一个通道,然后计算该整数的平方,并将结果发送到通道中。在 `main` 函数中,我们启动了多个 goroutines 来并行计算,并从通道中读取结果。
### 2. 使用 sync.WaitGroup
WaitGroup 是用来等待一组 goroutines 完成的。你可以在启动 goroutine 之前调用 `Add` 来设置计数器,然后在每个 goroutine 完成时调用 `Done`。
**例子:**
```go
package main
import (
"fmt"
"sync"
"time"
)
func square(number int, wg *sync.WaitGroup, results *[]int) {
defer wg.Done()
// 计算平方
result := number * number
// 模拟一些耗时
time.Sleep(time.Second)
// 将结果保存
*results = append(*results, result)
}
func main() {
var wg sync.WaitGroup
var results []int
numbers := []int{2, 4, 6, 8}
for _, number := range numbers {
wg.Add(1)
go square(number, &wg, &results)
}
wg.Wait()
fmt.Println(results)
}
```
在这个例子中,我们定义了一个 `square` 函数,它接受一个整数、一个指向 WaitGroup 的指针和一个指向结果数组的指针。每个 goroutine 完成时会调用 `Done` 方法。通过调用 `Wait` 方法,`main` 函数将等待所有 goroutine 完成后继续执行。
### 总结
使用通道和 WaitGroup 是处理 goroutines 并获取响应的两种常见方法。选择哪一种方法取决于具体的应用场景和个人偏好。通道非常适合于需要直接从 goroutines 传递数据的情况,而 WaitGroup 则适用于仅需等待一组操作完成的场景。
阅读 27 · 8月16日 02:26
Go 中接口类型的作用是什么?
在 Go 语言中,接口类型是一种非常强大的特性,主要用于定义对象的行为。接口定义了一组方法签名,任何实现了这些方法的类型都隐式地实现了该接口。这种设计方式具有几个重要作用:
1. **解耦合:** 接口帮助我们将实现细节从使用中分离出来。通过接口,我们不需要关心对象如何实现这些方法,只需要关心它可以做什么。这种抽象层面的设计使得代码更加灵活和可维护。
**例子:** 假设我们有一个 `Saver` 接口,它定义了一个 `Save` 方法。我们可以有多个实现,比如 `FileSaver` 用来将数据保存到文件中,`DBSaver` 用来保存到数据库。在其他代码中,我们只需要引用 `Saver` 接口,具体使用哪种保存方式可以灵活配置,甚至可以在运行时动态决定。
2. **多态:** 接口的另一个重要用途是实现多态。同一个接口的不同实现可以在不改变外部代码的情况下,有完全不同的行为。
**例子:** 继续上面的 `Saver` 接口的例子,我们可以在运行时根据不同的配置选择 `FileSaver` 或是 `DBSaver`,而调用它们的代码则无需任何改变,因为它们都实现了 `Saver` 接口。
3. **测试友好:** 接口使得单元测试变得更容易。我们可以创建一个接口的 mock 实现,用来在测试中替代真实的实现。这样可以在不依赖外部系统的情况下测试代码的逻辑。
**例子:** 如果我们要测试使用了 `Saver` 接口的代码,我们可以创建一个 `MockSaver` 实现,它记录下保存操作但不执行任何操作。这样我们可以在不触及文件系统或数据库的情况下测试该代码。
4. **设计灵活性:** 使用接口可以让我们的应用架构更加灵活。接口为代码提供了一种扩展的方式,使得我们可以在不修改现有代码的基础上扩展应用功能。
**例子:** 如果我们后续需要增加一个新的保存方式,例如保存到云存储,我们只需创建一个新的实现类 `CloudSaver`,实现 `Saver` 接口。现有的代码无需任何修改就可以支持新的保存方式。
总之,Go 中的接口类型是一种极其有用的工具,它通过提供清晰的抽象,支持良好的软件设计原则,如接口隔离、依赖倒置等,从而使得软件更加模块化,易于管理和扩展。
阅读 15 · 8月9日 03:00
Golang 中的字符串数据类型是什么,它是如何表示的?
在 Go 语言中,字符串是以 UTF-8 编码的不可变字节序列。字符串的数据类型在 Go 中被表示为 `string`。
每个字符串都是由一个或多个字节组成的序列,字节在 Go 中是 `uint8` 类型,这意味着它们是 8 位无符号整数。因为 Go 中的字符串采用 UTF-8 编码,所以每个 Unicode 码点(或称字符)可以由一个到四个字节表示。
这种设计使得 Go 的字符串可以很容易地处理多种语言的文本,同时保持对 ASCII 的高效支持。UTF-8 编码允许字符串有效地处理不同长度的字符,也便于网络传输和存储。
例如,考虑下面的 Go 程序代码:
```go
package main
import (
"fmt"
)
func main() {
greeting := "Hello, 世界"
fmt.Println(greeting)
fmt.Printf("Type of greeting: %T\n", greeting)
fmt.Printf("Length of greeting: %d bytes\n", len(greeting))
fmt.Printf("Number of runes in greeting: %d\n", len([]rune(greeting)))
}
```
在这个例子中,字符串 `greeting` 包含了英文单词 "Hello," 和中文词 "世界"。我们可以看到:
1. `greeting` 是一个 `string` 类型。
2. 使用 `len(greeting)` 得到的是字节长度,因为 "世界" 每个字符在 UTF-8 编码中占用 3 个字节,总共 6 个字节,加上 "Hello," 的 6 个字节和逗号空格两个字节,一共是 13 个字节。
3. 使用 `len([]rune(greeting))` 转换为 `rune` 切片后,我们可以得到字符的数量,这里是 9 个 Unicode 字符。
因此,Go 中的 `string` 类型非常适合处理国际化文本,同时也保持了良好的性能和灵活性。
阅读 15 · 8月9日 02:51