网上也有很多gohper总结了一些笔记,我再提炼精简一下,挂在脑图树上便于记忆。
值类型包括:所有integer、所有float、bool、string、数组和structure
引用类型包括:指针、slice、map、chan、interface
issue:定义、不初始化使用,运行时panic; solution:make关键字初始化或者使用map字面量
m := make(map[string]float64, 5)
m["pi"] = 3.14
mm := map[string]float64{"pi": 3.14, "pi1": 4.14} // map字面量
slice不需要初始化就可以被append.
var ss []int
ss = append(ss, 1, 2, 3)
fmt.Println(ss) // [1,2,3]
issue:
func main() {
x := map[string]string{"one":"a","three":"c"}
if v := x["two"]; v == "" { // 不存在该键值对,也能返回零值“”
fmt.Println("true")
}
}
solution: 使用map取值的参数2 bool值来判断
func main() {
x := map[string]string{"one":"a","two":"","three":"c"}
if _,ok := x["two"]; !ok {
fmt.Println("no entry")
}
}
issue:golang array 是值类型,形参是实参的内存拷贝
package main
import "fmt"
func changeFunc(arr [3]int) {
arr[0] = 222
}
func main() {
var arr [3]int = [3]int{1, 2, 3}
changeFunc(arr)
for i, item := range arr {
fmt.Printf("index : %d, item: %d \n\r", i, item)
}
}
solution:实参从array改成引用类型的 slice。
slice切片的实质是SliceHeader 结构体,值传递slice时,正好将底层数组指针拷贝。
type SliceHeader struct {
Data uintptr // 底层数组的指针
Len int
Cap int
}
issue: 块内声明的变量会遮蔽上层的同名变量n
func main() {
n := 0
if true {
n := 1
n++
}
fmt.Println(n) // 0
}
solution: 不要使用同名遮蔽变量,使用 n=1
issue:同大多数语言一样,golang的string是不可变的
func main() {
s := "hello"
s[0] = 'H'
}
# command-line-arguments
.\main.go:8:2: cannot assign to s[0] (value of type byte)
solution: 尝试通过byte/rune 中转
s := "hello"
buf := []rune(s)
buf[0] = 'H'
ss := string(buf)
fmt.Println(ss) // Hello
issue: strings.TrimRight实际是将cutset字符串拆成字符,然后原字符串从右向左,直到遇到没有在cutset中国出现的字符。
fmt.Println(strings.TrimRight("ABBA", "BA")) // ""
fmt.Println(strings.TrimRight("ABBAABABCABAB", "BA")) // "ABBAABABC"
solution:常规的移除后缀使用 strings.TrimSuffix
issue: 内置函数Copy
returns the number of elements copied, which will be the minimum of len(src) and len(dst).
src := []int{1, 2, 3}
var dst []int // 此时len(dst) =0
copy(dst, src)
fmt.Println(dst) // []
solution: 初始化足够空间的dst
src := []int{1, 2, 3}
var dst []int = make([]int, 3)
copy(dst, src)
fmt.Println(dst) // [1,2,3]
或者使用append方法
src := []int{1, 2, 3}
var dst1 []int
dst1 = append(dst1, src...)
fmt.Println(dst1) // [1,2,3]
或者使用append方法
src := []int{1, 2, 3}
var dst1 []int
dst1 = append(dst1, src...)
fmt.Println(dst1) // [1,2,3]
append方法既可以加元素,也可以加切片,真是活见久。 // slice = append(slice, elem1, elem2) // slice = append(slice, anotherSlice...)
issue:
func main() {
ss := []int{11, 4, 5, 2, 7}
for i, item := range ss {
fmt.Printf(" %d : %d \r\n", i, item)
}
mm := map[string]int{"11": 11, "4": 4, "5": 5, "2": 2, "7": 7}
for i, m := range mm {
fmt.Printf(" %s : %d \r\n", i, m)
}
}
输出:
0 : 11
1 : 4
2 : 5
3 : 2
4 : 7 // slice
4 : 4 // map
5 : 5
2 : 2
7 : 7
11 : 11
golang中除了经典的三段式for循环外,还有帮助快速遍历 slice array map channel的 for range循环。
issue1:for range中操作迭代变量,原切片竟然没影响。
func main() {
ss := []int{1, 1, 1}
for _, x := range ss {
x = x + 1
}
fmt.Println(ss) // [1,1,1]
}
solution:操作索引值
func main() {
ss := []int{1, 1, 1}
for i,_ := range ss {
ss[i] += 1
}
fmt.Println(ss) // [2,2,2]
}
issue2:这也是一个有意思的case, 迭代体内对于[修改array元素值]无意识, 对于[修改slice元素值]有意识, 活见久。
func main() {
aa := [2]int{0, 0}
for _, x := range aa {
fmt.Println(x) // print 0,0
aa[1] = 8
}
fmt.Println(aa) // print [0,8]
}
solution:将array换成slice
ss := []int{0, 0}
for _, x := range ss {
fmt.Println(x) // print 0,8
ss[1] = 8
}
fmt.Println(ss) // print [0,8]
以上问题的关键是:
所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量 ha,在赋值的过程中就发生了拷贝,而我们又通过 len 关键字预先获取了切片的长度,所以在循环中追加新的元素也不会改变循环执行的次数,这也就解释了上面提到的现象。
而遇到这种同时遍历索引和元素的 range 循环时,Go 语言会额外创建一个新的 v2 变量存储切片中的元素,循环中使用的这个变量 v2 会在每一次迭代被重新赋值而覆盖,赋值时也会触发拷贝。
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
tmp := ha[hv1]
v1, v2 = hv1, tmp
...
}
C#中没有这么多诡异的情况。
C#数组是定长数组,一旦被创建,数组大小就无法改变;
span 带有底层数组指针和长度,但是长度也是只读,是类型安全、内存安全的滑块。
issue: golang中:一个接口等于另一个接口,前提是它们的类型和动态值相同。这同样适用于nil值。
func Foo() error {
var err *os.PathError = nil
fmt.Println(err == nil) // print true
return err
}
func main() {
err := Foo()
fmt.Println(err) // print: <nil>
fmt.Println(err == nil) // print: false
}
solution:强转为同一类型
fmt.Println(err == (*os.PathError)(nil)) // print: true
或者显式返回nil error
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}
在底层,接口被实现为两个元素,一个类型T和一个值V,V是一个具体的值,比如int、结构体或指针,而不是接口本身,它的类型是T, 上面的错误示例中:err 具备了T=*MyError, V=nil
的实现,故与nil不等。
只要记住,如果接口中存储了任何具体的值,该接口将不会为nil.
最后再提供几张图,供大家参考,也许上面的坑位能柳暗花明。
(1)
[4]int在内存中的表示形式只是按顺序排列的四个整数值。
(2)
s = make([]byte,5)
切片[2]是数组片段的描述符,它由指向数组的指针、片段的长度和它的容量(片段的最大长度)组成。
当我们对s
进一步切片: s =s[2,4]
C# span是指向一段连续内存的类型安全的、内存安全的视图,也有数组指针和长度length,不过他的length是只读定长的,也不会有扩容的动作。
(3)
m := make(map[string]string)
m是指向Map Header数据结构的指针,Map Header
包含了关于map[3]的所有元信息:
https://phati-sawant.medium.com/internals-of-map-in-golang-33db6e25b3f8
[1]
nil值比较: https://golang.org/doc/faq#nil_error
[2]
切片: https://go.dev/blog/slices-intro
[3]
map: https://go.dev/blog/maps