云原生探索系列(三):Go基础语法 与 Python 对比(数据结构二)
前言
这片文章继续探讨Go编程语言的底层基础语法,并将其与 Python 进行若干比较分析(涵盖关键数据结构主题)。
Map
声明
声明Map可以通过两种方式,使用内建函数 make 或使用 map 关键字来定义。
func main() {
var myMap1 map[string]int
fmt.Println(myMap1 == nil)
myMap2 := map[string]int{}
fmt.Println(myMap2 == nil)
myMap3 := make(map[string]int, 10)
fmt.Println(myMap3 == nil)
}
编译运行,看看执行结果:
true
false
false
从输出结果可以看到,由于map是引用类型,如果仅声明而没有初始化,那么这个map就是nil map,它是无法直接使用的。
访问元素
func main() {
myMap3 := make(map[string]int, 10)
// 获取键值对
v, ok := myMap3["pear"] // 如果键不存在,ok 的值为 false,v2 的值为该类型的零值
fmt.Printf("v => %d, ok => %t\n", v, ok)
// 修改键值对
myMap3["one"] = 1
fmt.Printf("myMap3 map => %v\n", myMap3)
// 删除键值对
delete(myMap3, "one")
fmt.Printf("myMap3 map => %v\n", myMap3)
// 删除不存在的键值对
delete(myMap3, "two")
fmt.Printf("myMap3 map => %v\n", myMap3)
}
编译执行,看看输出内容:
v => 0, ok => false
myMap3 map => map[one:1]
myMap3 map => map[]
myMap3 map => map[]
可以看到在获取或者删除一个不存的键值对时不会报错。
python声明字典
# 直接声明一个空字典
my_dict = {}
# 带有初始元素的字典
my_dict = {'name': 'Alice', 'age': 25, 'city': 'New York'}
# 使用dict()函数声明字典
my_dict = dict(name='Alice', age=25, city='New York')
访问元素
# 通过键访问值
print(my_dict['name']) # 输出 'Alice'
# 使用get方法访问值
print(my_dict.get('age')) # 输出 25
添加或更新元素
# 添加新的键值对
my_dict['gender'] = 'female'
# 更新已有键的值
my_dict['age'] = 26
删除元素
# 删除指定键的键值对
del my_dict['city']
# 使用pop方法删除指定键的键值对
my_dict.pop('age')
不过需要注意的是,python中,如果访问(通过键访问)或者删除不存在的键,会报错的。如果是通过get访问,如果键不存在,默认会返回None.
Map延伸
键类型不能是哪些类型?
Go语言字典的键类型不可以是函数类型、字典类型和切片类型。 在键类型的值之间必须要支持判等操作。由于函数类型、字典类型和切片类型的值不支持判等操作,所以字典的键类型不能是这些类型。 如果键的类型是接口类型,那键值的实际类型也不可以是函数类型、字典类型和切片类型。 我们看下面这段代码:
func main() {
var badMap = map[interface{}]int{
"1": 1,
[]int{1, 2, 3}: 2,
3: 3,
}
fmt.Println(badMap)
}
上面代码,我们使用接口类型作为map的键,但第二个键值是切片类型。我们编译这段代码,不会报错。然后,我们执行代码,会报错panic: runtime error: hash of unhashable type []int
总结一下:为啥键类型的值必须支持判等操作?因为Go会先比较哈希值,如果哈希值相等,那会用键值再次进行比较,因为不同值的哈希值可能是相同的(哈希碰撞)。只有 键的哈希值和键值都相等,才能说明找到了匹配的键-元素对。如果键值类型无法判等,就没办法进行下去了
优先考虑哪些类型作为键类型?
遵循一个规则:求哈希和判等操作的速度越快,对应的类型就越适合作为键类型。 优先选用数值类型和指针类型。
nil map可以进行读写操作吗?
文章开头,map声明中,我们知道了nil map即只进行了声明而没有初始化,那在此map上进行操作会发生什么呢? 先看下面代码:
func main() {
var nilMap map[string]int
key := "one"
v, ok := nilMap[key]
fmt.Printf("v=%d, ok=%t\n", v, ok)
fmt.Printf("删除键值对key=%s\n", key)
delete(nilMap, key)
fmt.Println("增加键值对")
nilMap[key] = 1
}
编译运行,查看执行结果:
v=0, ok=false
删除键值对key=one
增加键值对
panic: assignment to entry in nil map
从输出结果可以知道,在nil的map上,除了添加键值对,其他操作都不会报错。添加操作,会在运行时抛出panic。
结构体
通过 type ... struct 关键字自定义结构体。
type Person struct {
name string
age int
}
func (person Person) String() string {
return fmt.Sprintf("name:%s, age:%d", person.name, person.age)
}
上述代码中,结构体类型Person,有两个字段,分别表示姓名、年龄。然后下边定义了一个叫String的方法,用来格式化输出结构体中的字段值。
func main() {
person := Person{name: "jay", age: 20}
fmt.Printf("The person information: %s\n", person.String())
}
这段代码使用字面量初始化了一个Person类型的值,并把它赋值给了变量person。这里我显示调用了String方法。其实也可以省略。 在Go语言中,可以为一个类型编写一个名为String的方法,来定义该类型的字符串表示形式。这个String方法不需要任何参数声明,但需要有string类型的结果声明。正因为这样, 在调用fmt.Printf()函数时,使用占位符%s就可以打印出person的字符串表示形式,无需显示调用。 接着,我们继续声明一个Student结构体
type Student struct {
Person // 嵌入 Person 结构体
School string
Grade int
name string // 隐藏 Person 结构体中的 name
}
这个结构体有个特别之处,其中一个字段是上面定义的Person结构体。字段声明Person代表了Student类型的一个嵌入字段。Go语言规范规定,如果一个字段的声明中只有字段的类型名而没有 字段的名称,拿它就是一个嵌入字段,也被称为匿名字段。 这里我先不写名为String的方法
func main() {
student := Student{
Person: Person{"hony", 20},
School: "MIT",
Grade: 1,
name: "学生1",
}
fmt.Printf("The student information: %s\n", student)
编译执行,会输出如下内容:
The student information: name:hony, age:20
代码中使用Printf函数和%s占位符打印student,相当于调用student的String方法,虽然这里没有为Student类型编写String方法,嵌入字段Person的String方法会被调用。 如果为Student类型编写String方法,那么Person的String方法将被屏蔽不会被调用。
func (student Student) String() string {
return fmt.Sprintf("person:%s, school:%s, grade:%d, studentName:%s", student.Person, student.School, student.Grade, student.name)
重新编译,执行,会输出如下内容
The student information: person:name:hony, age:20, school:MIT, grade:1, studentName:学生1
可以看到是Student类型编写String方法被调用了。 另外注意,我们看到Person和Student结构体中都有name字段,如果使用Student类型示例直接访问name,那将是访问Student类型的name,Person 结构体中的 name将被屏蔽。
值方法与指针方法
在 Go 语言中,可以给结构体定义方法。这些方法可以定义在结构体上,以便对结构体实���进行操作。在定义方法时,可以使用值接收者(value receiver)或指针接收者(pointer receiver)。这两种接收者有不同的行为和使用场景。
值方法
值方法使用结构体实例作为接收者。定义值方法时,方法接收者是结构体的一个副本,而不是结构体的指针。因此,在值方法内部对结构体字段的修改不会影响原始结构体实例。
type Person struct {
name string
age int
}
func NewPerson(name string, age int) Person {
return Person{name, age}
}
func (p Person) String() string {
return fmt.Sprint(p.name, " is ", p.age)
}
// 定义值方法
func (p Person) SetNameOfCopy(name string) {
p.name = name
}
指针方法
指针方法使用结构体指针作为接收者。定义指针方法时,方法接收者是结构体的指针,可以直接修改结构体的字段,并且修改会影响原始结构体实例。
// 定义指针方法
func (p *Person) SetName(name string) {
p.name = name
}
我们来测验一下输出结果:
func main() {
p := NewPerson("John", 20)
fmt.Printf("原Person实例: %s\n", p)
p.SetName("Mike")
fmt.Printf("调用指针方法修改名字后: %s\n", p)
p.SetNameOfCopy("MikeCopy")
fmt.Printf("调用值方法修改名字后: %s\n", p)
}
编译执行,输出如下内容:
原Person实例: John is 20
调用指针方法修改名字后: Mike is 20
调用值方法修改名字后: Mike is 20
从输出结果也可以看出,指针方法会改变原结构体实例。 我们可以通过p.SetName("Mike")修改名字,是因为Go语言把它自动转为了(&p).SetName("Mike"),即:先取p的指针值。然后在该指针值上调用SetName方法。
区别
- 值方法 :适用于不需要修改结构体字段的方法,以及对结构体实例进行操作而不需要修改原始数据的情况。值方法接收者是结构体的副本,方法内部对结构体字段的修改不会影响原始结构体实例。
- 指针方法 :适用于需要修改结构体字段的方法,以及对结构体实例进行直接修改的情况。指针方法接收者是结构体的指针,方法内部对结构体字段的修改会影响原始结构体实例。
在选择值方法和指针方法时,可以根据具体需求来决定。一般来说,如果方法需要修改结构体的字段,就应该使用指针方法;如果方法不涉及修改结构体字段,就可以考虑使用值方法。值方法和指针方法的选择可以影响程序的性能和行为,因此需要根据实际情况来进行权衡和选择。
Go语言中哪些值不可寻址
常量的值
func main() {
const num = 1
_ = &num
}
这样写编译就会报错:无法提取 'num' 的地址
基本类型值的字面量
func main() {
_ = &(1)
}
这样写编译就会报错:无法提取 '1' 的地址
算术操作的结果值
func main() {
_ = &(1 + 2)
}
这样写编译就会报错:无法提取 '1 + 2' 的地址
对各种字面量的索引表达式和切片表达式的结果值。不过有一个例外,对切片字面量的索引结果值却是可以寻址的
func main() {
_ = &([3]int{1, 2, 3}[0]) // 对数组字面量的索引结果值不可寻址。
_ = &([3]int{1, 2, 3}[0:1]) // 对数组字面量的切片结果值不可寻址。
_ = &([]int{1, 2, 3}[0]) // 对切片字面量的索引结果值却是可寻址的。
_ = &([]int{1, 2, 3}[0:2]) // 对切片字面量的切片结果值不可寻址。
}
这样写编译就会报错:无法提取 '[3]int{1, 2, 3}[0]' 的地址、无法提取 '[3]int{1, 2, 3}[0:1]' 的地址
对字符串变量的索引表达式和切片表达式的结果值
func main() {
var str = "hello"
_ = &(str[0]) // 对字符串变量的索引结果值不可寻址
_ = &(str[0:1]) // 对字符串变量的切片结果值不可寻址。
}
这样写编译就会报错:无法提取 'str[0]' 的地址、无法提取 'str[0:1]' 的地址
func main() {
var str = "hello"
str2 := str[0:1]
_ = &(str2)
}
注意:这样寻址是合法的。
对字典变量的索引表达式的结果值
func main() {
var map1 = map[int]string{1: "a"}
_ = &(map1[0])
}
这样写编译就会报错:无法提取 'map1[0]' 的地址 函数字面量和方法字面量,以及对它们的调用表达式的结果值
func main() {
_ = &(func(x, y int) int {
return x + y
}) // 字面量代表的函数不可寻址。
_ = &(fmt.Sprintf) // 标识符代表的函数不可寻址。
_ = &(fmt.Sprintln("abc")) // 对函数的调用结果值不可寻址。
}
这样写编译就会报错: 无法提取 'func(x, y int) int { return x + y }' 的地址、无法提取 'fmt.Sprintf' 的地址、无法提取 'fmt.Sprintln("abc")' 的地址
结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值
类型转换表达式的结果值 类型断言表达式的结果值 接收表达式的结果值
最后
尽管对于广大Python使用者而言,学习和理解Go语言可能会有一定的挑战性,但请不要气馁,让我们共同努力,继续深入研究并不断提升自己的技能水平。
转载自:https://juejin.cn/post/7388825326988394534