likes
comments
collection
share

云原生探索系列(三):Go基础语法 与 Python 对比(数据结构二)

作者站长头像
站长
· 阅读数 26

前言

这片文章继续探讨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方法。

区别

  1. 值方法 :适用于不需要修改结构体字段的方法,以及对结构体实例进行操作而不需要修改原始数据的情况。值方法接收者是结构体的副本,方法内部对结构体字段的修改不会影响原始结构体实例。
  2. 指针方法 :适用于需要修改结构体字段的方法,以及对结构体实例进行直接修改的情况。指针方法接收者是结构体的指针,方法内部对结构体字段的修改会影响原始结构体实例。

在选择值方法和指针方法时,可以根据具体需求来决定。一般来说,如果方法需要修改结构体的字段,就应该使用指针方法;如果方法不涉及修改结构体字段,就可以考虑使用值方法。值方法和指针方法的选择可以影响程序的性能和行为,因此需要根据实际情况来进行权衡和选择。

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
评论
请登录