文章目录
字符串和常用数据结构
使用字符串
第二次世界大战促使了现代电子计算机的诞生,最初计算机被应用于导弹弹道的计算,而在计算机诞生后的很多年时间里,计算机处理的信息基本上都是数值型的信息。世界上的第一台电子计算机叫ENIAC(电子数值积分计算机),诞生于美国的宾夕法尼亚大学,每秒钟能够完成约5000次浮点运算。随着时间的推移,虽然数值运算仍然是计算机日常工作中最为重要的事情之一,但是今天的计算机处理得更多的数据可能都是以文本的方式存在的,如果我们希望通过Python程序操作这些文本信息,就必须要先了解字符串类型以及与它相关的知识。
所谓字符串,就是由零个或多个字符组成的有限序列。在golang程序中,我们把单个或多个字符用者双引号"
包围起来,就可以表示一个字符串。
说明:golang除了双引号外,还支持使用反引号(`)来创建原始字符串,原始字符串是一种特殊的字符串字面量,它以反引号开始和结束,并且其中的内容会被原样保留,不进行任何转义字符的解析。
s1 := "Hello"
s2 := "world"
concatenated := s1 + ", " + s2 // "Hello, world"
//反引号创建的是原始字符串
rawStr := `This is a raw string with "quotes" and \n without being escaped.`
fmt.Println(rawStr) //This is a raw string with "quotes" and \n without being escaped.
可以在普通字符串中使用\
(反斜杠)来表示转义,也就是说\
后面的字符不再是它原来的意义,例如:\n
不是代表反斜杠和字符n,而是表示换行;而\t
也不是代表反斜杠和字符t,而是表示制表符。所以如果想在字符串中表示"
要写成\'
,同理想表示\
要写成\\
。可以运行下面的代码看看会输出什么。
s1 := "\"hello, world!\""
s2 := "\n\\hello, world!\\\n"
fmt.Println(s1, s2)
在\
后面还可以跟一个八进制或者十六进制数来表示字符,例如\141
和\x61
都代表小写字母a
,前者是八进制的表示法,后者是十六进制的表示法。也可以在\
后面跟Unicode字符编码来表示字符,例如\u4f60\u597d
代表的是中文“你好”。运行下面的代码,看看输出了什么。
s1 := "\141\142\143\x61\x62\x63"
s2 := "\u4f60\u597d"
fmt.Println(s1, s2)
如果不希望字符串中的\
表示转义,我们可以直接使用反引号`来创建字符串。
s1 := `\'hello, world!\'`
s2 := `\n\\hello, world!\\\n`
fmt.Println(s1, s2)
golang为字符串类型提供了非常丰富的运算符,我们可以使用+
运算符来实现字符串的拼接,我们也可以用[]
和[:]
运算符从字符串取出某个字符或某些字符(切片运算);另外strings
包内也提供了字符串的运算和操作方法,代码如下所示。
import(
"strings"
)
//字符串拼接
s1 := "Hello, "
s2 := "World!"
result := s1 + s2 // result = "Hello, World!"
//字符串子串
s3 := "Golang is fun"
substring := s[7:] // substring = "is fun"s1 += s2
//使用内建函数 len() 返回字符串的长度(以字节计),若需得到字符(rune)数量,使用 unicode/utf8 包中的 RuneCountInString() 函数
s4 := "你好,世界!"
byteLen := len(s) // byteLen = 1.png
charCount := utf8.RuneCountInString(s) // charCount = 4
//字符串查找
s5 := "Hello, World!"
contains := strings.Contains(s, "World") // contains = true
index := strings.Index(s, ",") // index = 7
//字符串替换
s6 := "apple, apple, orange"
replaced := strings.Replace(s, "apple", "banana", -1) // replaced = "banana, banana, orange"
//字符串拆分
s7 := "apple,banana,orange"
fruits := strings.Split(s, ",") // fruits = ["apple", "banana", "orange"]
//大小写转换
s8 := "Hello, World!"
lower := strings.ToLower(s) // lower = "hello, world!"
upper := strings.ToUpper(s) // upper = "HELLO, WORLD!"
//字符串修剪
s := " Hello, World! "
trimmed := strings.TrimSpace(s) // trimmed = "Hello, World!"
我们之前讲过,可以用下面的方式来格式化输出字符串。
package main
import (
"fmt"
)
func main(){
name := "Alice"
age := 30
fmt.Printf("Name: %s, Age: %d", name, age)
}
除了字符串,golang还内置了多种类型的数据结构,如果要在程序中保存和操作数据,绝大多数时候可以利用现有的数据结构来实现,最常用的包括数组、切片,指针,映射等。
使用指针
golang中,指针
是一种特殊类型,它存储的是另一个变量的内存地址。指针提供了对底层内存的直接访问能力,允许对所指向变量的值进行修改或通过其地址访问数据。
//声明一个指针
var ptr *int
//初始化指针
var value int = 42
ptr = &value // ptr 现在存储了 value 变量的地址
fmt.Println(*ptr) // 输出 42,解引用 ptr 并打印其指向的值
*ptr = .png // 将 ptr 指向的变量值修改为 .jpg
fmt.Println(value) // 输出 .jpg,因为 value 是 ptr 所指向的变量
指针常被用作函数参数,以使函数能够修改传入变量的原始值。这样可以避免在函数调用时复制整个数据结构,提高效率,特别是在处理大对象或结构体时。例如:
func incrementByOne(ptr *int) {
*ptr += 1
}
value := 10
incrementByOne(&value)
fmt.Println(value) // 输出 11,因为函数通过指针修改了 value 的值
使用数组(array)
和其他语言有所不同,golang的字符串类型(string
)和之前我们讲到的数值类型(int
和float
)有都是标量类型,也就是说这种类型的对象没有可以访问的内部结构;而接下来要说的类型array
是一种结构化的、非标量类型,会有一系列的属性和方法。数组(array
)它是一组相同类型元素的有序的,固定长度的集合,定义数组可以将数组的元素放在[]
中,多个元素用,
进行分隔,可以使用for
循环对列表元素进行遍历,也可以使用[]
或[:]
运算符取出列表中的一个或多个元素。
下面的代码演示了如何定义数组、如何遍历数组以及数组的下标运算。
//定义格式为var arrayName [arrayLength]elementType
var numbers [5]int
numbers[0] = 10
numbers[1] = 20
//也可以通过短变量方式定义
arr := [3]string{"apple", "banana", "cherry"}
// 类型推导(无需显式写出数组长度和类型)
anotherArr := [...]string{"pear", "orange", "kiwi"} // 编译器会根据元素数量和类型自动推断数组类型
//使用for循环遍历
var myArray [5]int = [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(myArray); i++ {
fmt.Println(myArray[i]) // 打印数组中的元素
}
//可以使用切片形式来表达
var numbers [10]int = [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 取子集,从索引 2(包含)到索引 5(不包含)
subArray := numbers[2:5]
// 取奇数位置的元素,步长为 2
oddNumbers := numbers[0:10:2]
使用切片(slice)
上述提到了切片,切片(Slice
是一种灵活的、动态大小的、基于数组的序列类型。切片提供了对数组某个连续片段的访问,但不像数组那样具有固定长度。切片本身并不直接存储数据,而是包含一个指向底层数组的指针、长度和容量。这种设计使得切片在很多方面类似于其他编程语言中的动态数组或列表。
下面的代码演示了如何向切片中添加元素以及如何从列表中移除元素。
var mySlice []int // 声明一个未初始化的整数切片
mySlice1 := []int{1, 2, 3, 4, 5} // 声明并初始化一个整数切片
//也可以使用make定义切片
mySlice2 := make([]int, 5) // 创建一个长度为 5、初始元素均为 0 的整数切片
// 可以指定初始容量(大于等于长度)
mySlice3 := make([]int, 5, 10) // 创建一个长度为 5、容量为 10 的整数切片
//添加多个元素
mySlice1 = append(mySlice1, 6, 7, 8)
// 合并一个切片
mySlice1 = append(mySlice1,mySlice2...)
//没有直接提供删除切片中某个元素的函数,可以创建一个新的切片,该切片包含原切片中除要删除元素之外的所有元素
// 删除索引为 2 的元素
mySlice1 = append(mySlice1[:2], mySlice1[3:]...)
//通过函数删除多个值匹配的元素
func removeValue(slice []int, value int) []int {
result := []int{}
for _, elem := range slice {
if elem != value {
result = append(result, elem)
}
}
return result
}
mySlice1 = removeValue(mySlice1,3)
排序也是切片可执行的操作之一,不引入其他包的情况下,需要我们用程序进行简单排序,也是很多算法的实际应用,以下是简单冒泡排序
package main
import (
"fmt"
)
func bubbleSort(numbers []int) {
n := len(numbers)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if numbers[j] > numbers[j+1] {
numbers[j], numbers[j+1] = numbers[j+1], numbers[j]
}
}
}
}
func main() {
nums := []int{5, .jpg", 2, .png", 3, 1}
bubbleSort(nums)
fmt.Println(nums)
}
下面的代码通过sort包实现了对列表的排序操作。
package main
import (
"fmt"
"sort"
)
func main() {
// 整数切片
ints := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}
sort.Ints(ints) // 升序排序
fmt.Println(ints)
// 浮点数切片
floats := []float64{3.14, 2.71, 1.618, 0.ⅈ, -e}
sort.Float64s(floats) // 升序排序
fmt.Println(floats)
// 字符串切片
strings := []string{"apple", "banana", "cherry", "date"}
sort.Strings(strings) // 升序排序
fmt.Println(strings)
}
使用字典(map)
Go 语言中的 map
(映射、字典)是一种内建的数据结构,用于存储键值对(key-value pairs),允许通过键(key)
快速查找、插入和删除对应的值(value)
。
以下是map的定义方法以及基础操作示例
// 声明 map 类型变量,
// var myMap map[keyType]valueType
var myMap map[string]string
// 使用 make 初始化 map
myMap = make(map[string]string)
// 或者直接声明并初始化
myMap := make(map[string]string)
//赋值
myMap["key"] = "value"
//查找,若不存在返回零值或引发错误
value := myMap["key"]
value, ok := myMap["key"]
if ok {
// 键存在,使用 value
} else {
// 键不存在
}
//删除
delete(myMap, "key")
//长度
len(myMap)
//遍历,map的遍历顺序是随机的
for key, value := range myMap {
// 处理 key 和 value
}
map
还有一些需要注意的事项
- 内部实现:Go 语言的
map
内部使用哈希表实现,其性能通常优于线性搜索。哈希表通过哈希函数将键转化为哈希值,再通过这个哈希值定位到桶(bucket),每个桶可以存放一个或多个键值对。当哈希冲突发生时,会通过链地址法(拉链法)解决冲突,即在同一桶内形成链表 - 扩容:随着元素数量的增长,map 会自动扩容以保持良好的性能。扩容会导致重新哈希所有元素,这是一个相对耗时的操作。可以通过预先指定较大的初始容量来减少扩容次数。
- 未初始化的
map
变量默认值为nil
。对nil map
进行操作(如插入、查找、删除)会引发panic
(错误)。因此,在使用map
之前,务必通过 make 函数进行初始化。
练习
练习1:在屏幕上显示跑马灯文字。
参考答案:
package main
import (
"fmt"
"time"
)
const text = "Hello, World! This is a scrolling marquee."
func main() {
width := 80 // 假设终端宽度为 80 字符
step := 1 // 每次移动的字符数
delay := 100 * time.Millisecond // 移动间隔
for {
for pos := 0; pos <= len(text); pos += step {
// 计算截取的文本范围
start := pos
end := pos + width
if end > len(text) {
end = len(text)
}
// 清除当前行
fmt.Print("\r\033[2K")
// 输出文本片段并移动光标至行首
fmt.Printf("%s\r", text[start:end])
// 延迟一段时间后继续移动
time.Sleep(delay)
}
}
}
练习2:设计一个函数产生指定长度的验证码,验证码由大小写字母和数字构成。
参考答案:
package main
import (
"math/rand"
"time"
)
const (
alphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
alphaLower = "abcdefghijklmnopqrstuvwxyz"
numbers = "0123456789"
allChars = alphaUpper + alphaLower + numbers
defaultLen = 6 // 可以根据需要设定默认验证码长度
maxAttempts = 10 // 防止随机生成时出现重复验证码的最大尝试次数
)
// GenerateCaptcha 生成指定长度的验证码,包含大小写字母和数字。
func GenerateCaptcha(length int) (string, error) {
if length <= 0 {
return "", fmt.Errorf("验证码长度必须大于0")
}
rand.Seed(time.Now().UnixNano()) // 初始化随机数生成器
var captcha string
attempts := 0
for len(captcha) < length || attempts > maxAttempts {
attempts++
if attempts > maxAttempts {
return "", fmt.Errorf("无法生成唯一验证码,达到最大尝试次数")
}
captcha = generateRandomString(length)
}
return captcha, nil
}
// generateRandomString 生成指定长度的随机字符串,包含大小写字母和数字。
func generateRandomString(length int) string {
b := make([]byte, length)
for i := range b {
b[i] = allChars[rand.Intn(len(allChars))]
}
return string(b)
}
func main() {
captcha, err := GenerateCaptcha(8)
if err != nil {
panic(err)
}
fmt.Println("Generated captcha:", captcha)
}
练习3:设计一个函数返回给定文件名的后缀名。
参考答案:
package main
import (
"fmt"
"path"
)
// GetFileSuffix 获取文件的后缀名
func GetFileSuffix(filename string) string {
// 使用path.Ext函数获取文件扩展名
ext := path.Ext(filename)
// 如果扩展名是空的,说明没有后缀,返回空字符串
if ext == "" {
return ""
}
// 返回后缀名,不包含点号
return ext[1:]
}
练习4:设计一个函数返回传入的列表中最大和第二大的元素的值。
参考答案:
func FindTopTwo(numbers []int) (int, int) {
if len(numbers) == 0 {
return 0, 0
} else if len(numbers) == 1 {
return numbers[0], 0
}
// 初始化最大和第二大值
max, secondMax := numbers[0], numbers[1]
if secondMax > max {
max, secondMax = secondMax, max
}
// 遍历切片,更新最大和第二大值
for i := 2; i < len(numbers); i++ {
if numbers[i] > max {
secondMax = max
max = numbers[i]
} else if numbers[i] > secondMax && numbers[i] != max {
secondMax = numbers[i]
}
}
return max, secondMax
}
练习5:计算指定的年月日是这一年的第几天。
参考答案:
package main
import (
"fmt"
"time"
)
// IsLeapYear 判断是否是闰年
func IsLeapYear(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
// DayOfYear 计算指定年月日是这一年的第几天
func DayOfYear(year int, month time.Month, day int) int {
daysInMonth := [13]int{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
if IsLeapYear(year) {
daysInMonth[2] = 29 // 闰年的2月有29天
}
sum := 0
for i := 1; i < int(month); i++ {
sum += daysInMonth[i]
}
sum += day
return sum
}
func main() {
year := 2023
month := time.September
day := 14
dayOfYear := DayOfYear(year, month, day)
fmt.Printf("The %dth day of the year %d is %s\n", dayOfYear, year, time.Date(year, month, day, 0, 0, 0, 0, time.UTC))
}
综合案例
案例1:双色球选号。
package main
import (
"fmt"
"math/rand"
"sort"
"time"
)
// SelectDoubleColorBalls 选号函数
func SelectDoubleColorBalls() ([]int, int) {
rand.Seed(time.Now().UnixNano()) // 使用当前时间的纳秒作为随机数种子
// 生成6个不重复的红球号码
redBalls := make([]int, 6)
for i := 0; i < 6; {
num := rand.Intn(33) + 1
if !contains(redBalls, num) {
redBalls[i] = num
i++
}
}
sort.Ints(redBalls) // 对红球号码进行排序
// 生成1个蓝球号码
blueBall := rand.Intn(16) + 1
return redBalls, blueBall
}
// contains 检查切片中是否包含某个元素
func contains(slice []int, num int) bool {
for _, v := range slice {
if v == num {
return true
}
}
return false
}
func main() {
redBalls, blueBall := SelectDoubleColorBalls()
fmt.Printf("红球号码: %v\n", redBalls)
fmt.Printf("蓝球号码: %d\n", blueBall)
}
说明: 上面使用rand包的Intn函数来实现伪随机数。