本文整理一下最近Go刷题时觉得有可能会理不清的几个点。

变量初始化,var还是new还是make?

首先说一下结论

  • var用于定义变量,定义变量的时候可以没有默认值,会依照变量类型自动指定默认值

  • new用于初始化struct

  • make一般用于初始化以下三种类型:slicemapchannel

下面逐个说明这三个关键词之间的区别

var

如果你也写过Java之类的语言,那么var其实可以形象的理解为是跟Java一样的创建变量的方式,他的行为也和Java等其它语言的行为大致相同——创建一个变量,如果没指定默认值的情况下就自动设置默认值。

那么设置默认值的规则是什么呢?

Golang和其他语言一样也有基础类型和复杂类型,对于几种基础类型,在Go tour有以下的描述

0 for numeric types, false for the boolean type, and "" (the empty string) for strings.

也就是说对于这几种基础类型,在创建变量并且未进行赋值的时候都会自动赋值以上的默认值(对于数字类型是0,布尔类型是false,字符串类型是空字符串)。

而对于其他数据类型,情况则稍微有所不同。

指针(Pointer) :指针类型默认值是nil。

结构体(Struct) :结构体默认值会初始化一个结构体,并且把各个字段按照基础类型的规则来赋值。

下面的代码演示了上面的规则:

package main

import "fmt"

type MyStruct struct {
   A int
   B string
   C bool
}

func main() {
   var a *MyStruct
   var b MyStruct
   fmt.Printf("%+v\n", a)
   fmt.Printf("%+v %t", b, b.B == "")
}

输出

<nil>
{A:0 B: C:false} true

函数类型(func) :默认值是nil

package main

import "fmt"

func main() {
   var f func()
   fmt.Printf("%t\n", f == nil)
}

输出

true

数组(array) :默认值会填充对应类型的默认值

这里以int为例

package main

import "fmt"

func main() {
   var arr [3]int
   fmt.Printf("%v", arr)
}

输出

[0 0 0]

切片(Slice)、Map :创建对应结构的空值

package main

import "fmt"

func main() {
   var arr []int
   fmt.Printf("%v\n", arr)
   var m map[int]int
   fmt.Printf("%v", m)
}

输出

[]
map[]

interface、channel :默认值是nil

package main

import "fmt"

func main() {
   var i interface{}
   fmt.Printf("%v\n", i)
   var c chan int
   fmt.Printf("%v", c)
}

输出

nil
nil

所以说 channel必须要make初始化之后才能使用。

new和make

  • new :为指定类型分配内存,设置零值,并返回指针 (分配内存的位置不固定,取决于逃逸分析)

  • make :仅用于 slice, map, channel 的创建与初始化, 并且返回对象本身(不是指向对象的指针)

new主要的过程是如上所述,分配内存并设置零值,注意它返回的是指向对应内存区域的指针。

new不止可以操作struct,他可以初始化任何类型的变量,比如下面这种操作是合法的。

package main

import "fmt"

func main() {
   i := new(int)
   // 注意这里一定要手动解引用,否则输出的是内存地址,这里不会自动解引用
   fmt.Printf("%d", *i)
}

golang 1.26之前的new不可以指定默认值

而make主要是用来初始化slice、map、channel这三种类型。这三种类型仅仅去直接分配内存空间是不够的,所以在分配内存之后,需要做进一步处理才可以。

比如slice,slice其实很多人可能对他的名字“切片”感到误解。其实slice和其他语言类似功能的组件(比如Java的ArrayList)是有一些相似在里面的,只是用起来不是那么的整体(各种append、len,还有array试图操作,看起来不像是和slice在一个整体),关于他的细节在下面会讲解。slice不是数组那种很简单的内存结构。对于slice来讲需要更多的其实需要操作初始化他的表头,也就是记录slice各种信息的一个结构。

而对于map则是需要初始化hashmap对应的结构。channel需要初始化各种内部缓冲区和同步机制。

make返回的是对象本身,不是指针

使用上基本也没什么好说的了。

Go 1.26的new更改

https://tip.golang.org/doc/go1.26#language

RIP to all the PointerTo[T any](in T) *T functions we all made as soon as 1.18 dropped.

数组相关

Slice的常用操作

  • 取长度:len(arr)

  • 底层数组容量上限:cap(arr)

  • 拷贝:copy(dst, src),实际拷贝数量是min(len(dst), len(src))(也就是说dst要初始化长度)

  • 截取:arr[low:high](左闭右开)

  • 删除i位置的元素:s = append(s[:i], s[i+1:]…)创建一个新slice,跳过中间的i

  • 追加元素(尾插):s = append(s, …)

  • 头插:s = append({}T{item}, s...)(创建一个新数组)

Slice底层究竟是如何存的

{“link_preview”:{“url”:“https://github.com/golang/go/blob/master/src/runtime/slice.go"}}slice头结构

type slice struct {
   array unsafe.Pointer
   len   int
   cap   int
}

slice扩容 append

len(arr) = cap(arr)时触发扩容

  • 当原 slice 容量 (oldcap) 小于 256 的时候,新slice(newcap)容量为原来的2倍;

  • 原 slice 容量超过 256,新 slice 容量newcap = oldcap+(oldcap+3*256)/4

Slice切片

slice切片操作是arr[low:high],对于下标左开右闭。和python行为一致。

切片操作会生成一个新的slice头,然后映射到原来slice底层数组上的对应区域。

于是就有了这种很有趣的现象:

package main

import (
   "fmt"
)

func main() {
   arr := []int{1, 2, 3, 4, 5}
   newArr := arr[2:5]
   fmt.Printf("%v\n", arr)
   fmt.Printf("%v\n", newArr)
   arr[3] = 9
   fmt.Printf("%v\n", arr)
   fmt.Printf("%v\n", newArr)
}

其实切片完整写法是arr[low:high:cap],第三个cap是生成的新的slice的容量上限。

这里面也会有一个很有趣的现象:如果切片之后,触发了扩容,那么原slice的之后一部分数据也会跟着进入新slice。

Slice作为函数参数,传的是指针还是值

首先明确一点, Golang的函数参数都是值传递 ,即使是结构体也是会复制一份过去的,传引用是需要手动定义指针的。

而如果在其他函数中尝试修改元素时,修改依然会反应到外侧,是因为修改元素的过程实际上动的时slice的底层数组。

Golang在传slice时,会把slice头完整的复制给内层函数中,这个slice头和外面的slice指向的是同一个底层数组。

基于这一点,可以确认的一点是,这种情况符合预期的操作,仅限于去修改slice元素,涉及到需要动底层数组的操作(比如append)都会让函数内外行为不一致。

认识容器类:list.List

golang的list.List是golang自带的链表实现,实现了很多的链表操作,比如:

  • 创建:list.New()

  • 头尾插入:PushFront(item)PushBack(item)PushFrontList(item)PushBackList(item)

  • 前后插入:InsertBefore(item)InsertAfter(item)

  • 头尾元素 :Front()Back()

  • 移除元素:Remove(item)(返回被移除的元素)

没有泛型。

package main

import (
   "container/list"
   "fmt"
)

// printList 是一个辅助函数,用于清晰地打印链表的内容
func printList(l *list.List) {
   fmt.Print("List content: [ ")
   // 从头到尾遍历
   for e := l.Front(); e != nil; e = e.Next() {
   	// e.Value 是一个 interface{} 类型,需要进行类型断言
   	fmt.Printf("%v ", e.Value)
   }
   fmt.Println("]")
   fmt.Printf("Length: %d\n\n", l.Len())
}

func main() {
   // 1. 初始化 (Initialization)
   // 创建一个新的空链表
   tasks := list.New()
   fmt.Println("--- 1. Initial Empty List ---")
   printList(tasks)

   // 2. 添加元素 (Adding Elements)
   fmt.Println("--- 2. Adding Elements ---")
   // PushBack: 在链表尾部添加元素 (Enqueue)
   tasks.PushBack("Task 2: Buy groceries")
   tasks.PushBack("Task 3: Pay bills")
   fmt.Println("After PushBack:")
   printList(tasks)

   // PushFront: 在链表头部添加元素
   // PushFront 会返回一个 *list.Element 指针,我们可以保存它以便后续操作
   firstTaskElement := tasks.PushFront("Task 1: Walk the dog")
   fmt.Println("After PushFront:")
   printList(tasks)

   // 3. 插入元素 (Inserting Elements)
   fmt.Println("--- 3. Inserting Elements ---")
   // InsertAfter: 在指定元素(第一个任务)之后插入一个新任务
   tasks.InsertAfter("Task 1.5: Feed the dog", firstTaskElement)
   fmt.Println("After InsertAfter 'Task 1':")
   printList(tasks)

   // InsertBefore: 在指定元素(第一个任务)之前插入一个新任务
   urgentTaskElement := tasks.InsertBefore("Urgent Task: Call mom", firstTaskElement)
   fmt.Println("After InsertBefore 'Task 1':")
   printList(tasks)

   // 4. 遍历与访问 (Traversing and Accessing)
   fmt.Println("--- 4. Traversing and Accessing ---")
   fmt.Println("Forward traversal:")
   for e := tasks.Front(); e != nil; e = e.Next() {
   	fmt.Printf("  - %s\n", e.Value.(string))
   }

   fmt.Println("\nBackward traversal:")
   for e := tasks.Back(); e != nil; e = e.Prev() {
   	fmt.Printf("  - %s\n", e.Value.(string))
   }
   fmt.Println()

   // Front() 和 Back() 用于获取头尾元素
   fmt.Printf("First task is: '%v'\n", tasks.Front().Value)
   fmt.Printf("Last task is: '%v'\n\n", tasks.Back().Value)


   // 5. 移动元素 (Moving Elements)
   fmt.Println("--- 5. Moving Elements ---")
   // MoveToFront: 将紧急任务移动到链表头部
   tasks.MoveToFront(urgentTaskElement)
   fmt.Println("After moving urgent task to front:")
   printList(tasks)
   
   // MoveToBack: 将第一个任务移动到链表尾部
   tasks.MoveToBack(firstTaskElement)
   fmt.Println("After moving 'Task 1' to back:")
   printList(tasks)
   
   // MoveAfter: 将紧急任务移动到“买菜”任务之后
   // 首先需要找到“买菜”这个元素
   var groceriesElement *list.Element
   for e := tasks.Front(); e != nil; e = e.Next() {
   	if e.Value.(string) == "Task 2: Buy groceries" {
   		groceriesElement = e
   		break
   	}
   }
   if groceriesElement != nil {
   	tasks.MoveAfter(urgentTaskElement, groceriesElement)
   	fmt.Println("After moving urgent task after 'Buy groceries':")
   	printList(tasks)
   }

   // 6. 删除元素 (Removing Elements)
   fmt.Println("--- 6. Removing Elements ---")
   // Remove: 删除指定的元素 (这里我们删除刚刚移动的紧急任务)
   removedValue := tasks.Remove(urgentTaskElement)
   fmt.Printf("Removed element with value: '%v'\n", removedValue)
   fmt.Println("After removing urgent task:")
   printList(tasks)

   // 7. 链表拼接与清空 (Combining and Clearing)
   fmt.Println("--- 7. Combining and Clearing ---")
   // 创建另一个链表
   otherTasks := list.New()
   otherTasks.PushBack("Extra Task 1")
   otherTasks.PushBack("Extra Task 2")
   fmt.Println("Another list to be combined:")
   printList(otherTasks)

   // PushBackList: 将一个链表的所有元素附加到另一个链表的尾部
   tasks.PushBackList(otherTasks)
   fmt.Println("After combining lists:")
   printList(tasks)

   // Init: 清空链表,使其恢复到初始状态
   tasks.Init()
   fmt.Println("After clearing the list with Init():")
   printList(tasks)
}

为什么能不要用链表就不要用链表

  • CPU的三缓会整段加载array,找前后元素会很快,如果是链表的话这部分就吃不到CPU缓存,造成一定的性能低下。

  • 找下一个节点这个操作会影响CPU的指令流水线和预取 (Pipelining & Prefetching)。CPU 无法预测下一个节点在哪里,只能一步一步地等待内存响应,执行效率极低。(这个不太懂)

  • 链表的Node需要额外存储其他节点的内存地址,浪费内存

Sort包

sort包是golang的排序工具类的合集,他包含了一些常用的排序方法。

比如

  • sort.Ints(slice []int)对 int 切片进行升序排序。

  • sort.Float64s(slice []float64)对 float64 切片进行升序排序。

  • sort.Strings(slice []string)对 string 切片进行字典序升序排序。

请记住 这些方法都是升序排序 ,并且 只能传Slice.

1.8之后提供了一个函数叫sort.Slice,可以直接传一个匿名函数进去辅助排序。

sort.Slice(people, func(i, j int) bool {
   return people[i].Age < people[j].Age // 按年龄升序
})

这个包还提供了一些稳定排序的实现,比如SliceStable这个的意思是遇到重复值的时候保证这些重复值的先后顺序跟之前的一样。

sort包也提供了二分查找实现,传进来的slice必须是升序好的Slice。返回的是下标。

i := sort.Search(len(a), func(i int) bool {
   return a[i] >= x 
})

也提供了翻转方法:

numbers = []int{4, 1, 8, 3, 6}
sort.Sort(sort.Reverse(sort.IntSlice(numbers)))

字符串操作相关

rune和byte的区别

  • byteuint8的alias

  • runeint32的alias

  • Golang规定String是utf-8的,byte类型无法包含所有可用字符,所以需要rune来做处理

字符串运算相关

对于一个字符串来讲,计算有几个字符,安全的做法是使用utf8.RuneCountInString(s)

string类型底层是只读[]byte,如果直接使用下标访问字符串,得到的是对应的byte而不是rune,如果你的字符串里有非ASCII字符的话,这样访问会产生问题。

如果需要访问字符串某个下标位置的值,可以手动转换成rune slice:[]rune(s)[i]

for i, v := range a这样的v,会被golang底层自动处理成rune,所以不用考虑上诉问题。

strconv

目标函数示例
string ➔ intstrconv.Atoi(s)i, err := strconv.Atoi("123")
int ➔ stringstrconv.Itoa(i)s := strconv.Itoa(123)
string ➔int64strconv.ParseInt(s, base, bitSize)v, _ := strconv.ParseInt("FF", 16, 64)
int64➔ stringstrconv.FormatInt(i, base)s := strconv.FormatInt(255, 16)
string ➔boolstrconv.ParseBool(s)b, _ := strconv.ParseBool("true")
bool➔ stringstrconv.FormatBool(b)s := strconv.FormatBool(true)
string ➔float64strconv.ParseFloat(s, bitSize)f, _ := strconv.ParseFloat("3.14", 64)
float64➔ stringstrconv.FormatFloat(f, fmt, prec, bitSize)s := strconv.FormatFloat(3.14, 'f', 2, 64)
  • bitSize原始类型是多少位的,做溢出检查的,不能超过这么多位的数据结构

  • fmt

    • f普通float格式:1.23
- `g`自动选择