簡介
Go
中的 切片(slice
) 是 Go
最強大、最常用的數據結構之一。它是對數組的輕量封裝,比數組更靈活,幾乎所有的集合處理都用切片來完成。
什么是切片(slice)
切片是一個擁有 長度(len
)和容量(cap
) 的 動態數組視圖。底層是一個數組,但可以動態擴容、共享數組。
var s []int // nil slice,len=0,cap=0
fmt.Println(s, len(s), cap(s)) // [] 0 0s := []int{} // 空切片,已初始化但無元素
切片的創建
使用字面量
s := []int{1, 2, 3}
從數組或切片切割而來
arr := [5]int{0, 1, 2, 3, 4}
s := arr[1:4] // 包含索引1~3:1, 2, 3
使用 make 創建
s := make([]int, 3) // len=3, cap=3,默認值0
s := make([]int, 3, 5) // len=3, cap=5
切片表達式
-
簡單表達式:
s[low:high]
,左閉右開區間。 -
完整表達式:
s[low:high:max]
,指定容量為max-low
,用于限制后續操作的容量。
切片的底層結構
type slice struct {ptr *T // 底層數組指針len int // 當前長度cap int // 容量(底層數組的最大長度)
}
切片只是一個“視圖窗口”,多個切片可能共享同一數組。
切片的切割和操作
s := []int{10, 20, 30, 40, 50}
s1 := s[1:4] // [20 30 40]
s2 := s[:3] // [10 20 30]
s3 := s[2:] // [30 40 50]
s4 := s[:] // 全部復制
常見操作
刪除元素
s := []int{1, 2, 3, 4}
s = append(s[:2], s[3:]...) // 刪除索引2 → [1, 2, 4]
清空切片
s = s[:0] // 長度置0,保留容量
s = nil // 釋放底層數組
切片的遍歷
可以使用 for
循環或者 for...range
循環來遍歷切片。
package mainimport "fmt"func main() {slice := []int{1, 2, 3, 4, 5}// 使用 for 循環遍歷fmt.Println("Using for loop:")for i := 0; i < len(slice); i++ {fmt.Println(slice[i])}// 使用 for...range 循環遍歷fmt.Println("Using for...range loop:")for index, value := range slice {fmt.Printf("Index: %d, Value: %d\n", index, value)}
}
高級技巧
預分配容量:減少擴容次數,提升性能。
s := make([]int, 0, 1000) // 預分配容量
避免內存泄漏:截取大切片中的小部分后,復制到新切片以釋放原數組。
bigSlice := make([]int, 1000000)
smallPart := make([]int, 10)
copy(smallPart, bigSlice[:10])
字符串處理
字符串可視為 []
byte切片,但需注意中文字符需轉為 []rune
str := "你好"
runes := []rune(str) // 正確處理中文字符
切片的容量擴展與底層數組共享
s := []int{1, 2, 3}
s1 := s[:2] // [1 2]
s2 := append(s1, 99) // 可能修改 s 底層內容fmt.Println(s)
如果
append
后容量沒超出原數組,s1
與s
仍然共享底層數組。
append、copy 的用法
append 用于追加元素
s := []int{1, 2}
s = append(s, 3, 4) // [1 2 3 4]
slice = append(slice, anotherSlice...) // 追加另一個切片(使用...展開)
copy 用于切片復制
a := []int{1, 2, 3}
b := make([]int, 2)
copy(b, a) // 只復制前2個元素
函數傳參時的切片特性
切片本質是引用類型,傳參是復制切片結構體值,但底層數組是共享的。
func modify(s []int) {s[0] = 999
}
s := []int{1, 2, 3}
modify(s)
fmt.Println(s) // [999 2 3]
多維切片(二維數組)
matrix := [][]int{{1, 2, 3},{4, 5, 6},
}
matrix[1][2] = 9
多維切片是切片的切片,并不是嚴格的二維數組結構。
切片常見問題與陷阱
共享底層數組導致修改影響原切片
s := []int{1, 2, 3, 4}
s1 := s[:2]
s1[0] = 999
fmt.Println(s) // [999 2 3 4]
超過容量自動分配新數組
s := make([]int, 2, 3)
s = append(s, 4) // 使用原數組
s = append(s, 5) // 超過cap,創建新數組
判斷空切片
使用 len(s) == 0
而非 s == nil
,因空切片可能非 nil
推薦使用方式總結
場景 | 推薦寫法 |
---|---|
初始化切片 | make([]T, len, cap) 或 []T{...} |
安全擴容 | s = append(s, x...) |
不修改原切片 | new := append([]T(nil), old...) |
復制切片 | copy(dst, src) |
清空切片 | s = s[:0] 或 var s []T |
growslice 實現解析(簡化版)
func growslice(et *_type, old slice, cap int) slice {// 參數解釋:// et : 元素類型指針// old : 原 slice 數據結構(包括 len, cap, ptr)// cap : 需要的新容量newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {if old.len < 1024 {newcap = doublecap} else {// 對于大 slice,增幅是 1.25 倍for newcap < cap {newcap += newcap / 4}}}// 分配新數組(unsafe.Pointer)newptr := mallocgc(et.size * uintptr(newcap), et, true)// 拷貝原數據到新內存memmove(newptr, old.array, et.size * uintptr(old.len))// 構造新的 slice 對象return slice{newptr, old.len, newcap}
}
slice 結構體
type slice struct {array unsafe.Pointerlen intcap int
}
分配和拷貝函數詳解
-
mallocgc
:分配一塊 heap 內存(等價于make([]T, cap)
)。 -
memmove
:底層是匯編,類似memcpy
。 -
et.size
:每個元素的大小。
append 背后的操作
-
cap
足夠:原地追加,新slice
與舊共享數組 -
cap
不夠:分配新數組、復制原數據、添加新元素 -
多次
append
:容量以倍數或 1.25 倍增長,最多增長到 2? 或更多 -
返回值:一定是新
slice
,即使底層沒變 -
類型安全:編譯器會根據
T
類型生成對growslice
的調用
growslice 的效率與陷阱
-
復制開銷:每次擴容都需要拷貝舊數據(
O(n)
復雜度); -
內存浪費:新數組會有空余空間(以倍數擴容);
-
共享底層數組:沒擴容時,多個
slice
會共享底層數組,易出錯。
數組 vs 切片 GC 行為對比
特性 | 數組([N]T) | 切片([]T) |
---|---|---|
是否固定大小 | 是否固定大小 | 否 |
存儲位置 | 值類型,復制時復制整個內容 | 引用類型,復制的是頭結構 |
GC 行為 | 如果被釋放,整體數組會被 GC | 如果原數組被引用,即使只用一部分,整個數組仍保留 |
是否共享底層內存 | 不會共享 | 可能多個切片共享底層數組 |
GC 是否會收回未使用部分 | 若無引用,整體釋放 | 若有切片引用,整個底層數組不會釋放 |
內存泄露風險
func getSlice() []byte {large := make([]byte, 1<<20) // 1MBreturn large[:100] // 返回一個小切片,但引用了整個大數組
}
上面這段代碼雖然只返回了 100 個字節的切片,但實際上因為這個切片仍然引用了整個 1MB 的底層數組,GC 不會釋放整個數組,直到這個小切片本身不再被引用。
解決方法
func getCopiedSlice() []byte {large := make([]byte, 1<<20)result := make([]byte, 100)copy(result, large[:100])return result // 這樣只引用小數據
}
如何避免 GC 泄漏
場景 | 建議做法 |
---|---|
截取切片時只需部分數據 | 使用 copy() 拷貝到新切片 |
數據生命周期短 | 注意避免長生命周期切片引用大量數據 |
要釋放大數組 | 確保沒有切片引用底層數組 |
想清空切片 | 使用 s = nil 而不是 s = s[:0] (后者不會釋放內存) |
使用切片來模擬分頁
核心功能:
-
模擬有很多行的表格數據
-
支持分頁查看(指定頁碼、每頁條數)
-
支持翻頁(上一頁、下一頁)
-
支持跳轉頁數
示例效果(CLI)
$ go run main.go
[Page 1/10] Showing 10 of 100 rows:
1. Name: Alice Age: 25
2. Name: Bob Age: 30
...[n] Next Page | [p] Previous Page | [q] Quit | [g 5] Go to page 5
>
項目結構
table-paginator/
├── main.go // 啟動入口
├── paginator/
│ └── paginator.go // 分頁邏輯
├── data/
│ └── mock.go // 模擬表格數據
核心代碼框架預覽
- data/mock.go
package datatype Person struct {Name stringAge int
}func GenerateMockData(total int) []Person {people := make([]Person, total)for i := 0; i < total; i++ {people[i] = Person{Name: fmt.Sprintf("User%03d", i+1),Age: 20 + (i % 30),}}return people
}
- paginator/paginator.go
package paginatorimport "fmt"type Page[T any] struct {Items []TPageNo intPageSize intTotalItems int
}func Paginate[T any](data []T, pageNo, pageSize int) Page[T] {total := len(data)start := (pageNo - 1) * pageSizeend := start + pageSizeif start > total {start = total}if end > total {end = total}return Page[T]{Items: data[start:end],PageNo: pageNo,PageSize: pageSize,TotalItems: total,}
}
- main.go
package mainimport ("bufio""fmt""os""strings""table-paginator/data""table-paginator/paginator"
)func main() {people := data.GenerateMockData(100)pageSize := 10pageNo := 1reader := bufio.NewReader(os.Stdin)for {page := paginator.Paginate(people, pageNo, pageSize)fmt.Printf("\n[Page %d/%d] Showing %d of %d rows:\n",page.PageNo,(page.TotalItems+pageSize-1)/pageSize,len(page.Items),page.TotalItems)for i, p := range page.Items {fmt.Printf("%2d. Name: %-10s Age: %d\n", (page.PageNo-1)*pageSize+i+1, p.Name, p.Age)}fmt.Print("\n[n] Next Page | [p] Previous Page | [g 5] Go to page 5 | [q] Quit\n> ")input, _ := reader.ReadString('\n')input = strings.TrimSpace(input)switch {case input == "n":pageNo++case input == "p":if pageNo > 1 {pageNo--}case strings.HasPrefix(input, "g "):var newPage intfmt.Sscanf(input, "g %d", &newPage)if newPage >= 1 {pageNo = newPage}case input == "q":returndefault:fmt.Println("Invalid command")}}
}
Go slice切片跟Java的List和C#.NET的List異同
相同點(共性)
特性 | 描述 |
---|---|
動態增長 | 都可以在運行時動態擴容,無需指定固定長度 |
支持下標訪問 | 可以通過 [] 操作符訪問和修改元素 |
有長度和容量(或大小) | 有長度和容量(或大小) |
內部基于數組實現 | 內部基于數組實現 |
支持切片/子列表(部分) | Go、Java(subList() )、C#(GetRange() )都可以實現 |
不同點(詳細對比)
特性 | Go slice | Java List | C# List<T> |
---|---|---|---|
所在語言 | Go | Java | C# (.NET) |
是否原生類型 | ? 是語言內建類型 | ? 是接口(通常用 ArrayList 等) | ? 是類 |
是否線程安全 | ? 否 | ? 否(Collections.synchronizedList 可封裝) | ? 否(要手動加鎖) |
內部擴容機制 | 默認 翻倍/1.25倍 擴容 | 增長約 50%(ArrayList) | 每次翻倍擴容(默認實現) |
內存管理 | 自動垃圾回收 | 自動垃圾回收 | 自動垃圾回收 |
切片是否共享底層數組 | ? 會共享 | ? subList() 是新對象引用同數組 | ? GetRange() 拷貝數據 |
底層可見性 | 有 len 和 cap 可查看 | 只有 size() | 有 Count 和 Capacity |
可否使用 append() | ? 內置 append() | ? 使用 add() 或 addAll() | ? 使用 Add() 或 AddRange() |
動態擴容機制
Go Slice
使用 append
追加元素時,若容量不足,按以下規則擴容:
- 容量 < 1024:雙倍擴容
- 容量 ≥ 1024:按 1.25 倍擴容,擴容后生成新底層數組,原數組可能被垃圾回收。
Java ArrayList
默認擴容為當前容量的 1.5 倍(如初始容量 10 → 15 → 22 → …)
C# List<T>
擴容時容量翻倍(如初始容量 4 → 8 → 16 → …)
常用操作對比
操作 | Go Slice | Java List | C# List<T> |
---|---|---|---|
添加元素 | append(slice, element) | add(element) | Add(element) |
刪除元素 | 通過 append 拼接前后片段 | remove(index) 或 remove(object) | RemoveAt(index) 或 Remove(obj) |
截取子集 | s[start:end] (共享底層數組) | subList(from, to) | GetRange(from, count) |
容量預分配 | make([]T, len, cap) | new ArrayList<>(initialCapacity) | new List<T>(capacity) |
拷貝 | copy(dst, src) (淺拷貝) | new ArrayList<>(srcList) | new List<T>(srcList) |
切片典型的代碼示例
切片配合結構體(最常見)
- 示例一
package mainimport "fmt"type User struct {ID intName string
}func main() {users := []User{{ID: 1, Name: "Alice"},{ID: 2, Name: "Bob"},}for _, user := range users {fmt.Printf("ID: %d, Name: %s\n", user.ID, user.Name)}
}
- 示例二
package mainimport ("fmt""sort"
)// 定義結構體
type Person struct {Name stringAge int
}// 結構體方法(值接收者)
func (p Person) String() string {return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}// 結構體方法(指針接收者)
func (p *Person) Grow() {p.Age++
}func main() {// 創建結構體切片people := []Person{{"Alice", 25},{"Bob", 30},{"Charlie", 20},}// 遍歷切片(值傳遞)for _, p := range people {fmt.Println(p)}// 修改結構體(需使用指針切片)peoplePtr := []*Person{{"Alice", 25},{"Bob", 30},{"Charlie", 20},}for _, p := range peoplePtr {p.Grow() // 調用指針接收者方法}// 按年齡排序sort.Slice(people, func(i, j int) bool {return people[i].Age < people[j].Age})fmt.Println("Sorted:", people)// 動態添加元素people = append(people, Person{"David", 40})
}
關鍵點:
-
使用指針切片(
[]*Person
)避免結構體復制開銷 -
結合
sort.Slice
實現自定義排序 -
通過
append
動態添加元素 -
示例三:模擬
List
的Add
方法
// 定義泛型結構體,包含切片字段
type Container[T any] struct {Elements []T
}// 添加元素方法(使用泛型接收器)
func (c *Container[T]) Add(element T) {c.Elements = append(c.Elements, element)
}// 使用示例
intContainer := Container[int]{Elements: []int{1, 2}}
intContainer.Add(3) // Elements: [1,2,3]strContainer := Container[string]{Elements: []string{"a", "b"}}
strContainer.Add("c") // Elements: [a,b,c]
切片配合接口(處理多種類型)
- 示例一
package mainimport "fmt"type Shape interface {Area() float64
}type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }func (c Circle) Area() float64 { return 3.14 * c.Radius * c.Radius }
func (r Rectangle) Area() float64 { return r.Width * r.Height }func printAreas(shapes []Shape) {for _, s := range shapes {fmt.Printf("Area: %.2f\n", s.Area())}
}func main() {shapes := []Shape{Circle{Radius: 3},Rectangle{Width: 4, Height: 5},}printAreas(shapes)
}
- 示例二
// 定義數值類型約束接口
type Numeric interface {int | float64
}// 泛型函數:計算切片元素和
func Sum[T Numeric](s []T) T {var total Tfor _, v := range s {total += v}return total
}// 使用示例
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.1, 2.2})) // 3.3
關鍵點:Numeric
接口通過類型集合約束切片元素類型,確保操作合法性
- 示例三
// 定義數據訪問接口
type DataStore[T any] interface {GetAll() []TAdd(item T)
}// 實現接口的泛型結構體
type GenericSliceStore[T any] struct {data []T
}func (g *GenericSliceStore[T]) GetAll() []T {return g.data
}func (g *GenericSliceStore[T]) Add(item T) {g.data = append(g.data, item)
}// 使用示例
store := &GenericSliceStore[int]{data: []int{10}}
store.Add(20)
fmt.Println(store.GetAll()) // [10,20]
關鍵點:接口通過泛型類型參數定義方法,結構體實現接口時綁定具體類型
切片配合泛型(Go 1.18+)
- 示例一
package mainimport "fmt"// 定義泛型函數,打印任何類型切片
func PrintSlice[T any](items []T) {for _, item := range items {fmt.Println(item)}
}func main() {ints := []int{1, 2, 3}strs := []string{"Go", "Rust", "Python"}PrintSlice[int](ints)PrintSlice[string](strs)
}
- 示例二
package mainimport "fmt"// 泛型過濾函數(過濾滿足條件的元素)
func Filter[T any](slice []T, test func(T) bool) []T {result := make([]T, 0)for _, v := range slice {if test(v) {result = append(result, v)}}return result
}// 泛型映射函數(轉換元素類型)
func Map[T any, U any](slice []T, mapper func(T) U) []U {result := make([]U, len(slice))for i, v := range slice {result[i] = mapper(v)}return result
}// 泛型結構體(存儲切片)
type Repository[T any] struct {Data []T
}func (r *Repository[T]) Add(item T) {r.Data = append(r.Data, item)
}func main() {// 過濾整數切片numbers := []int{1, 2, 3, 4, 5}evenNumbers := Filter(numbers, func(n int) bool {return n%2 == 0})fmt.Println("Even numbers:", evenNumbers) // [2 4]// 轉換字符串切片為長度切片words := []string{"apple", "banana", "cherry"}lengths := Map(words, func(s string) int {return len(s)})fmt.Println("Word lengths:", lengths) // [5 6 6]// 使用泛型結構體repo := Repository[string]{Data: []string{"Go", "Rust"}}repo.Add("C++")fmt.Println("Repository:", repo.Data) // [Go Rust C++]
}
關鍵點:
-
使用
T any
定義泛型類型參數 -
泛型函數支持類型安全操作(
Filter、Map
) -
泛型結構體(
Repository[T]
)封裝切片操作 -
示例三:模擬隊列
利用切片實現泛型隊列,支持動態類型存儲:
type Queue[T any] []T// 入隊方法
func (q *Queue[T]) Enqueue(item T) {*q = append(*q, item)
}// 出隊方法(返回泛型零值)
func (q *Queue[T]) Dequeue() T {if len(*q) == 0 {return *new(T) // 返回類型T的零值}item := (*q)[0]*q = (*q)[1:]return item
}// 使用示例
var q Queue[string]
q.Enqueue("first")
q.Enqueue("second")
fmt.Println(q.Dequeue()) // "first"
關鍵點:通過 any
類型約束實現多類型隊列,利用切片特性動態調整
- 示例四
// 用戶訂單結構體
type UserOrder[T comparable] struct {UserID TItems []string // 字符串切片字段
}// 泛型訂單管理器
type OrderManager[K comparable, V any] struct {Orders map[K][]V // 鍵值對中值類型為切片
}// 添加訂單方法
func (m *OrderManager[K, V]) Add(key K, value V) {m.Orders[key] = append(m.Orders[key], value)
}// 使用示例
manager := OrderManager[int, string]{Orders: make(map[int][]string)}
manager.Add(1001, "itemA")
manager.Add(1001, "itemB") // Orders[1001]: [itemA, itemB]
關鍵點:結合 map
和切片實現多層級泛型數據存儲,支持復雜業務場景
- 示例五
// 切片去重(需comparable約束)
func Deduplicate[T comparable](s []T) []T {seen := make(map[T]bool)result := []T{}for _, v := range s {if !seen[v] {seen[v] = trueresult = append(result, v)}}return result
}// 使用示例
nums := []int{1, 2, 2, 3}
strs := []string{"a", "a", "b"}
fmt.Println(Deduplicate(nums)) // [1 2 3]
fmt.Println(Deduplicate(strs)) // [a b]
- 示例六
func SliceToMap[T any, K comparable](s []T, keyFunc func(T) K) map[K]T {m := make(map[K]T)for _, item := range s {m[keyFunc(item)] = item}return m
}// 使用示例
userMap := SliceToMap(users, func(u User) int { return u.ID })
// 輸出map[1:{1 Alice} 2:{2 Bob} 3:{3 Charlie}]
切片 + 泛型 + 結構體(組合使用)
- 示例一
package mainimport "fmt"type User struct {ID intName string
}func FilterSlice[T any](items []T, filter func(T) bool) []T {var result []Tfor _, item := range items {if filter(item) {result = append(result, item)}}return result
}func main() {users := []User{{ID: 1, Name: "Alice"},{ID: 2, Name: "Bob"},{ID: 3, Name: "Eve"},}filtered := FilterSlice(users, func(u User) bool {return u.ID%2 == 1 // 只保留奇數 ID})for _, u := range filtered {fmt.Println(u)}
}
- 示例二
package mainimport "fmt"// 定義接口
type IDisplay interface {Display() string
}// 實現接口的結構體
type Product struct {Name stringPrice float64
}func (p Product) Display() string {return fmt.Sprintf("%s ($%.2f)", p.Name, p.Price)
}type User struct {Username stringEmail string
}func (u User) Display() string {return fmt.Sprintf("%s <%s>", u.Username, u.Email)
}// 泛型容器(要求類型實現 IDisplay 接口)
type DisplayBox[T IDisplay] struct {Items []T
}func (b *DisplayBox[T]) Add(item T) {b.Items = append(b.Items, item)
}func (b *DisplayBox[T]) ShowAll() {for _, item := range b.Items {fmt.Println(item.Display())}
}func main() {// 創建容器并添加不同類型數據box := DisplayBox[IDisplay]{}box.Add(Product{"Laptop", 999.99})box.Add(User{"Alice", "alice@example.com"})box.ShowAll()// 輸出:// Laptop ($999.99)// Alice <alice@example.com>
}
關鍵點:
-
結合接口約束(
T IDisplay
)實現類型安全 -
泛型容器存儲接口類型切片
-
統一調用接口方法(
item.Display()
)
- 示例三
type Identifiable interface {GetID() int
}type Product struct {ID intName stringPrice float64
}
func (p Product) GetID() int { return p.ID }// 通用查詢函數
func FindByID[T Identifiable](items []T, id int) (T, bool) {for _, item := range items {if item.GetID() == id {return item, true}}var zero Treturn zero, false
}// 使用示例
products := []Product{{1, "Laptop", 999.9}}
result, found := FindByID(products, 1)
- 多級切片處理
type Matrix[T any] [][]Tfunc (m Matrix[T]) Flatten() []T {var result []Tfor _, row := range m {result = append(result, row...)}return result
}// 使用示例
intMatrix := Matrix[int]{{1,2}, {3,4}}
strMatrix := Matrix[string]{{"a","b"}, {"c"}}
fmt.Println(intMatrix.Flatten()) // [1 2 3 4]
fmt.Println(strMatrix.Flatten()) // [a b c]
切片 + 泛型接口 + 排序(高級組合)
package mainimport ("fmt""slices" // Go 1.21+ 官方 slices 工具包
)type Person struct {Name stringAge int
}func main() {people := []Person{{"Alice", 25},{"Bob", 19},{"Eve", 31},}// 按 Age 排序slices.SortFunc(people, func(a, b Person) int {return a.Age - b.Age})fmt.Println(people)
}
總結
-
切片 + 結構體:最常見的組合,適合表示表格、列表等數據結構
-
切片 + 接口:支持多態,適合處理多種類型(如圖形、設備等)
-
切片 + 泛型:類型安全、復用性強,適合通用算法/工具方法
-
切片 + 泛型 + 結構體:結構化數據處理 + 高性能泛型,寫庫非常合適
通用分頁 + 泛型過濾 + 可視化表格
功能
-
任意結構體類型的數據切片
-
泛型過濾(支持傳入條件函數)
-
分頁(頁碼 + 每頁大小)
-
Web UI 表格展示(用 Go Serve HTML)
目錄結構
slice-table-demo/
├── main.go
├── data.go // 模擬數據和數據結構
├── pagination.go // 泛型分頁 + 過濾
├── templates/
│ └── index.html
代碼示例
- data.go — 模擬數據定義
package maintype User struct {ID intName stringEmail stringAge int
}func GetMockUsers() []User {users := make([]User, 100)for i := range users {users[i] = User{ID: i + 1,Name: "User_" + string('A'+(i%26)),Email: fmt.Sprintf("user%d@example.com", i+1),Age: 18 + (i % 30),}}return users
}
- pagination.go — 泛型分頁與過濾
package mainfunc FilterSlice[T any](items []T, filter func(T) bool) []T {var result []Tfor _, item := range items {if filter(item) {result = append(result, item)}}return result
}func PaginateSlice[T any](items []T, page, pageSize int) []T {start := (page - 1) * pageSizeend := start + pageSizeif start >= len(items) {return []T{}}if end > len(items) {end = len(items)}return items[start:end]
}
- templates/index.html — Web 表格模板(帶分頁)
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>User Table</title><style>table { border-collapse: collapse; width: 100%; }th, td { padding: 8px; border: 1px solid #ccc; text-align: left; }</style>
</head>
<body><h1>User List (Page {{.Page}})</h1><table><thead><tr><th>ID</th><th>Name</th><th>Email</th><th>Age</th></tr></thead><tbody>{{range .Users}}<tr><td>{{.ID}}</td><td>{{.Name}}</td><td>{{.Email}}</td><td>{{.Age}}</td></tr>{{end}}</tbody></table><p><a href="/?page={{.PrevPage}}">Prev</a> |<a href="/?page={{.NextPage}}">Next</a></p>
</body>
</html>
- main.go — 啟動 HTTP Server
package mainimport ("fmt""html/template""net/http""strconv"
)type PageData struct {Users []UserPage intPrevPage intNextPage int
}func main() {tmpl := template.Must(template.ParseFiles("templates/index.html"))http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {pageSize := 10pageStr := r.URL.Query().Get("page")page, _ := strconv.Atoi(pageStr)if page <= 0 {page = 1}users := GetMockUsers()// 過濾條件:例如只顯示年齡 > 25 的用戶filtered := FilterSlice(users, func(u User) bool {return u.Age > 25})pagedUsers := PaginateSlice(filtered, page, pageSize)data := PageData{Users: pagedUsers,Page: page,PrevPage: max(1, page-1),NextPage: page + 1,}tmpl.Execute(w, data)})fmt.Println("Listening on http://localhost:8080")http.ListenAndServe(":8080", nil)
}func max(a, b int) int {if a > b {return a}return b
}
運行
go run main.go# 瀏覽器訪問:http://localhost:8080
總結與選型建議
場景 | 推薦方案 | 優勢 |
---|---|---|
固定結構集合 | 結構體切片 | 直觀、類型安全 |
跨類型數據操作 | 泛型切片+接口約束 | 代碼復用率高 |
高性能批處理 | 預分配切片+數組重用 | 減少GC壓力 |
復雜數據結構 | 嵌套泛型結構體 | 靈活擴展多維數據 |
類型轉換優化 | 泛型轉換工具函數 | 避免冗余代碼 |
slices 包處理切片(slice)的常見操作
常用函數
-
slices.Sort
:對切片排序(需要元素可比較) -
slices.BinarySearch
:二分查找(已排序切片) -
slices.Index
:查找第一個等于給定值的索引 -
slices.Contains
:判斷切片中是否包含某值 -
slices.Equal
:判斷兩個切片是否相等(順序和值都一樣) -
slices.Clone
:拷貝一個切片 -
slices.Compact
:移除相鄰重復值(適合排好序的切片) -
slices.Delete
:刪除指定索引范圍的元素 -
slices.Insert
:插入元素到切片中 -
slices.Reverse
:反轉切片
使用 slices 包示例
package mainimport ("fmt""slices"
)func main() {ages := []int{32, 18, 45, 21, 18}// 排序slices.Sort(ages)fmt.Println("Sorted:", ages)// 查找idx := slices.Index(ages, 45)fmt.Println("Index of 45:", idx)// 是否包含fmt.Println("Contains 21:", slices.Contains(ages, 21))// 去重(只去相鄰重復)dedup := slices.Clone([]int{1, 1, 2, 2, 2, 3})slices.Compact(dedup)fmt.Println("Compact:", dedup)// 插入inserted := slices.Insert([]int{1, 2, 4}, 2, 3)fmt.Println("After insert:", inserted)// 刪除deleted := slices.Delete(inserted, 1, 3) // 刪除第1到3個元素fmt.Println("After delete:", deleted)
}
多個 goroutine 并發修改切片
使用 sync.Mutex 加鎖 來保證并發安全
package mainimport ("fmt""sync"
)func main() {var (slice []intmu sync.Mutexwg sync.WaitGroup)for i := 0; i < 10; i++ {wg.Add(1)go func(val int) {defer wg.Done()mu.Lock()slice = append(slice, val)mu.Unlock()}(i)}wg.Wait()fmt.Println("Final slice:", slice)
}
-
每個
goroutine
在append
前先加鎖,操作完成后釋放鎖。 -
sync.Mutex
保證了append
的原子性,避免了競態。
使用 channel 做串行通信,避免共享內存
package mainimport ("fmt""sync"
)func main() {slice := []int{}ch := make(chan int)var wg sync.WaitGroup// 啟動一個 goroutine 專門負責寫入切片go func() {for val := range ch {slice = append(slice, val)}}()// 啟動多個生產者 goroutinefor i := 0; i < 10; i++ {wg.Add(1)go func(val int) {defer wg.Done()ch <- val}(i)}wg.Wait()close(ch) // 所有寫入完成后關閉通道fmt.Println("Final slice:", slice)
}
-
所有寫入通過
channel
串行進行,避免了加鎖。 -
這是 Go 推薦的方式:不要通過共享內存來通信,而應該通過通信來共享內存。
對比總結
方法 | 優點 | 缺點 |
---|---|---|
sync.Mutex | 簡單高效,適合低沖突場景 | 容易死鎖,復雜操作難管理 |
channel | 更符合 Go 哲學,邏輯清晰 | 需要一個寫入協程,性能略低 |