数据结构分类
分类 | 说明 |
---|---|
基本类型 | 整型( int/uint/int8/uint8/int16/uint16/int32/uint32/int64/uint64/byte/rune等)浮点数( float32/float64)复数类型( complex64/complex128)字符串( string) |
引用类型 | 切片(slice)、channel、map、指针 |
复合类型(又叫聚合类型) | 数组和结构体类型 |
接口类型 | 如error |
rune类型
官方定义
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
//int32的别名,几乎在所有方面等同于int32
//它用来区分字符值和整数值
type rune = int32
补充:golang中的字符有两种,uint8(byte)代表ASCII的一个字符,rune代表一个utf-8字符。
使用
当需要处理中文、日文或者其他复合字符时,则需要用到rune类型,rune实际是一个int32
package main
import "fmt"
func main() {
var str = "hello 你好"
fmt.Println("len(str):", len(str))
for i := 0; i < len(str); i++ {
fmt.Printf("%c",str[i])
}
}
//输出结果
hello ä½ å¥½
golang中,**汉字采用utf-8编码,占用三个字节,编码后的值是int类型。字母采用ASCII编码。**那么如何获取有汉字的字符长度呢?
补充:若为gbk编码,则占用两个子节。
解决方案
采用for range遍历方法
字符串
字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。
在Go语言中字符串根据需要占用 1 至 4 个字节,而Java 始终使用 2 个字节。Go语言这样做不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。
定义字符串
可以使用双引号""来定义字符串,字符串中可以使用转义字符来实现换行、缩进等效果,常用的转义字符包括:
- \n:换行符
- \r:回车符
- \t:tab 键
- \u 或 \U:Unicode 字符
- \:反斜杠自身
package main
import (
"fmt"
)
func main() {
var str = "SHOPLINE\nGo语言教程"
fmt.Println(str)
}
运行结果为:
SHOPLINE
Go语言教程
字符串的内容(纯字节)可以通过标准索引法来获取,在方括号[ ]内写入索引,索引从0开始计数:
- 字符串 str 的第 1 个字节:str[0]
- 第 i 个字节:str[i - 1]
- 最后 1 个字节:str[len(str)-1]
需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。
注意:获取字符串中某个字节的地址属于非法行为,例如 &str[i]。
字符串拼接
字符串拼接的几种方式
字符串拼接在golang 里面有很多种实现,不同实现方式的性能差异比较大,下面来看一下
1. +号拼接
golang里面的字符串都是不可变的,每次运算都会产生一个新的字符串。如果是需要多次for循环调用等,会产生很多临时的无用的字符串,给 gc 带来额外的负担,性能会比较差
func BenchmarkAddStringWithOperator(b *testing.B) {
hello := "hello"
world := "world"
result := ""
for i := 0; i < b.N; i++ {
for i := 0; i < num; i++ {
result = result + hello + "," + world
}
}
}
2. strings.Join()
join会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,一个一个字符串填入,在已有一个数组的情况下,这种效率会很高,但是本来没有,去构造这个数据的代价也不小
func BenchmarkAddStringWithJoin(b *testing.B) {
hello := "hello"
world := "world"
result := ""
for i := 0; i < b.N; i++ {
for i := 0; i < num; i++ {
result = strings.Join([]string{result, hello, world}, ",")
}
}
}
3. strings.Builder
func BenchmarkAddStringWithBuilder(b *testing.B) {
hello := "hello"
world := "world"
var builder = strings.Builder{}
for i := 0; i < b.N; i++ {
for i := 0; i < num; i++ {
builder.WriteString(hello)
builder.WriteString(world)
}
_ = builder.String()
}
}
4. buffer.WriteString()
func BenchmarkAddStringWithBuffer(b *testing.B) {
hello := "hello"
world := "world"
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
for i := 0; i < num; i++ {
buffer.WriteString(hello)
buffer.WriteString(",")
buffer.WriteString(world)
}
_ = buffer.String()
}
}
这个比较理想,可以当成可变字符使用,对内存的增长也有优化,如果能预估字符串的长度,还可以用 buffer.Grow() 接口来设置 capacity
测试结果
到文件目录下执行命令
go test -bench=. -run=none -benchmem
num=1
BenchmarkAddStringWithOperator-12 140378 99534 ns/op 776122 B/op 1 allocs/op
BenchmarkAddStringWithJoin-12 107691 99395 ns/op 650173 B/op 1 allocs/op
BenchmarkAddStringWithBuilder-12 100000000 43.90 ns/op 57 B/op 0 allocs/op
BenchmarkAddStringWithBuffer-12 23336616 46.01 ns/op 64 B/op 1 allocs/op
num=10
BenchmarkAddStringWithOperator-12 10000 584710 ns/op 5540147 B/op 10 allocs/op
BenchmarkAddStringWithJoin-12 10000 921049 ns/op 6040229 B/op 10 allocs/op
BenchmarkAddStringWithBuilder-12 4227034 248.0 ns/op 555 B/op 0 allocs/op
BenchmarkAddStringWithBuffer-12 4710817 239.9 ns/op 304 B/op 3 allocs/op
num=100次时效果
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAddStringWithOperator-12 1803 14436238 ns/op 99570435 B/op 105 allocs/op
BenchmarkAddStringWithJoin-12 1563 18182929 ns/op 94185034 B/op 106 allocs/op
BenchmarkAddStringWithBuilder-12 998404 3393 ns/op 5745 B/op 0 allocs/op
BenchmarkAddStringWithBuffer-12 682932 1848 ns/op 3008 B/op 6 allocs/op
num=1000
BenchmarkAddStringWithOperator-12 100 53647278 ns/op 554014801 B/op 1036 allocs/op
BenchmarkAddStringWithJoin-12 100 85493944 ns/op 604022630 B/op 1037 allocs/op
BenchmarkAddStringWithBuilder-12 48417 23019 ns/op 60653 B/op 0 allocs/op
BenchmarkAddStringWithBuffer-12 80964 14287 ns/op 42944 B/op 10 allocs/op
从上面可以看到,WithBuffer的性能要好很多, 每次执行时间更短,分配的内存更小,内存分配的次数的也更少。num越大,效果越明显
定义多行字符串
在Go语言中,使用双引号书写字符串的方式是字符串常见表达方式之一,被称为字符串字面量(string literal),这种双引号字面量不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用`反引号,代码如下:
const str = `第一行
第二行
第三行
\r\n
`
fmt.Println(str)
代码运行结果:
第一行
第二行
第三行
在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
多行字符串一般用于内嵌源码和内嵌数据等,代码如下:
const codeTemplate = `// Generated by github.com/davyxu/cellnet/
protoc-gen-msg
// DO NOT EDIT!{{range .Protos}}
// Source: {{.Name}}{{end}}
package {{.PackageName}}
{{if gt .TotalMessages 0}}
import (
"github.com/davyxu/cellnet"
"reflect"
_ "github.com/davyxu/cellnet/codec/pb"
)
{{end}}
func init() {
{{range .Protos}}
// {{.Name}}{{range .Messages}}
cellnet.RegisterMessageMeta("pb","{{.FullName}}", reflect.TypeOf((*{{.Name}})(nil)).Elem(), {{.MsgID}}) {{end}}
{{end}}
}
`
这段代码只定义了一个常量 codeTemplate,类型为字符串,使用`定义,字符串的内容为一段代码生成中使用到的 Go 源码格式。
在`间的所有代码均不会被编译器识别,而只是作为字符串的一部分
结构体
定义结构体
结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:
type person struct {
age int
name string
}
结构体命名规范
结构体命名规范见:go编码规范,其中首字母根据访问控制决定使用大写或小写,大写表示公开,相当于java中的public。小写表示私有,相当于java中的private
此处为语雀内容卡片,点击链接查看:https://shopline.yuque.com/staff-gznel5/xtozqy/wo5x1hsh2nqdhuk3#kz7IH
结构体初始化
创建指针类型结构体
还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址:
var p = new(person)
fmt.Printf("%T\n", p) //*main.person
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"", city:"", age:0}
可以看出p是一个结构体指针
需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。
var p = new(person)
p.name = "张三"
p.age = 18
p.city = "深圳"
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"张三", city:"深圳", age:18}
取结构体的地址实例化
使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
book := &Books{
title:"golang",
author:"xxx",
subject:"实例化"
book_id:6495700
}
book.title= "Java"其实在底层是(*book).title= “Java”,这是Go语言实现的语法糖。
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
func main() {
var user struct{Name string; Age int}
user.Name = "张三"
user.Age = 20
fmt.Printf("%#v\n", user)
}
空结构体
空结构体是不占用空间的。
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
空结构体的作用
因为空结构体不占据内存空间,因此被广泛作为各种场景下的占位符使用。一是节省资源,二是空结构体本身就具备很强的语义,即这里不需要任何值,仅作为占位符。可以用做map实现set
指针
Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
以下实例演示了变量在内存中地址
package main
import "fmt"
func main() {
var a int = 10
fmt.Printf("变量的地址: %x\n", &a )
}
输出结果为
变量的地址: 20818a220
指针声明
一个指针变量可以指向任何一个值的内存地址
类似于变量和常量,在使用指针前需要声明指针。指针声明如下:
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
Go 空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
nil 指针也称为空指针。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
一个指针变量通常缩写为 ptr。
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("ptr 的值为 : %v\n", ptr )
fmt.Printf("ptr 的值为 : %#v\n", ptr )
}
输出结果为
ptr 的值为 : <nil>
ptr 的值为 : (*int)(nil)
指针使用注意事项
- 指针可以为空:在使用指针时,需要先判断指针是否为空,以避免空指针引起的问题。
- 指针可以指向任何类型的变量:需要注意指针的类型,不能将一个指针赋给不同类型的指针。
- 操作指针时需要小心:需要确保指针只指向有效的内存区域,否则会导致不可预测的结果。
- 不要滥用指针:虽然指针可以提高程序的效率,但是滥用指针会导致代码变得难以理解和维护,因此需要谨慎使用
函数
函数定义
Go 语言函数定义格式如下:
func function_name( [parameter list] ) [return_types]{
函数体
}
函数定义解析:
- func:函数由 func 开始声明
- function_name:函数名称,函数名和参数列表一起构成了函数签名。
- parameter list]:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 函数体:函数定义的代码集合
例如
函数接收两个参数,并交互顺序返回回去
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("Mahesh", "Kumar")
fmt.Println(a, b)
}
匿名函数和闭包
Golang支持匿名函数和闭包,可以在函数内部定义函数并使用外部变量。匿名函数的定义方式和普通函数类似,但没有函数名。闭包是指一个函数和与其相关的引用环境组合而成的实体,它可以访问外部函数的局部变量和参数
下面是一个使用匿名函数和闭包的示例
func increment() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
inc := increment()
fmt.Println(inc()) // 输出 1
fmt.Println(inc()) // 输出 2
fmt.Println(inc()) // 输出 3
}
闭包优点
闭包给访问外部函数定义的内部变量创造了条件。也将关于函数的一切封闭到了函数内部,减少了全局变量(个人认为的主要作用),原本需要新建结构体来做的事,通过闭包更快的实现。实际上
闭包:外部函数定义的内部函数就是闭包。
与普通函数的区别:
1,普通函数也能曝光内部的值。方法A定义全局变量,但占用的内存无法释放且函数使用的变量定义到了函数外部不便于理解和管理。方法B将内部变量当参数传递,此种方法不美观太丑陋。
2,函数每次执行时都会且只会初始化其内部变量,导致了闭包与普通函数的最大区别。就是每次调用普通函数时它内部都被初始化成一致状态,导致执行的结果是一致的。闭包不同,它的本质是内部函数,调用闭包只会初始化内部函数变量,外部函数的变量没有被初始化,实现了变量值的传递。外部函数只在定义闭包时被初始化。闭包消亡时内存被回收。
什么时候需要使用闭包:
当每次调用函数A时都要改变全局变量B,且B只与A有关。以往没有闭包时只能将B定义为全局变量,现在可以将B定义为A的内部变量,同时在A内部定义闭包C,并将C当值返回
函数参数传递
在Golang中,函数参数可以按值传递、按指针传递或按引用传递。按值传递是指将参数的副本传递给函数,函数对参数的修改不会影响原始变量的值。按指针传递是指将参数的地址传递给函数,函数可以通过指针修改原始变量的值。按引用传递是指将参数的引用传递给函数,函数可以直接修改原始变量的值。
下面是一个示例,展示不同参数传递方式的效果
func main() {
num := 1
fmt.Println("Before calling, num is", num)
// 按值传递
incrementByValue(num)
fmt.Println("After calling incrementByValue, num is", num)
// 按指针传递
incrementByPointer(&num)
fmt.Println("After calling incrementByPointer, num is", num)
// 按引用传递
incrementByReference(&num)
fmt.Println("After calling incrementByReference, num is", num)
}
func incrementByValue(num int) {
num++
}
func incrementByPointer(num *int) {
*num++
}
func incrementByReference(num **int) {
**num++
}
defer语句
https://segmentfault.com/a/1190000019303572
Go语言中的defer语句会将其后面跟随的语句进行延迟处理
在defer所属的函数即将返回时,将延迟处理的语句按照defer定义的顺序逆序执行,即先进后出
package main
import "fmt"
func main() {
fmt.Println("开始")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("结束")
}
以上代码执行结果如下
开始
结束
3
2
1
方法
Golang中的方法是一种与结构体类型关联的函数。定义方法时需要指定接收者类型,即要操作的对象类型。方法定义格式如下:
func (接收者类型) 方法名(参数列表) 返回值类型 {
// 方法体
return 返回值
}
其中,接收者类型可以是结构体类型或指针类型。下面是一个定义在结构体类型上的方法示例:
上述方法greet可以通过Person对象调用,例如:
func Person struct {
name string
age int
}
func (p Person) greet() {
fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.name, p.age)
}
p := Person{"John", 30}
p.greet() // 输出 Hello, my name is John and I'm 30 years old.
接口
在Golang中,接口是包含一类方法的数据结构,定义为
type name interface{
method1(param_list) return_type
method2(param_list) return_type
....
methodn(param_list) return_type
}
接口的作用是抽象化这些方法,即metho1,method2,⋯ \cdots⋯,methodn是没有实体的函数名。这些方法具体的行为由具体结构体或变量确定
举例说明
package main
import (
"fmt"
"reflect"
)
type A struct{
}
func(a *A)handle(x,y int){
fmt.Println(reflect.TypeOf(a),x+y)
}
type B struct{
}
func(b *B)handle(x,y int){
fmt.Println(reflect.TypeOf(b),x*y)
}
type Face interface{
handle(x,y int)
}
func main() {
fmt.Println("******Type A*****")
var a *A
var c Face
c=a;
c.handle(1,2);
fmt.Println("******Type B*****")
var b *B
c=b;
c.handle(1,2)
}
执行结果如下
******Type A*****
*main.A 3
******Type B*****
*main.B 2
接口类型
Go 语言根据接口类型是否包含一组方法将接口类型分成了两类:
- 使用 runtime.iface结构体表示包含方法的接口
- 使用 runtime.eface结构体表示不包含任何方法的 interface{} 类型
type eface struct { // 16 字节
_type *_type
data unsafe.Pointer
}
type iface struct { // 16 字节
tab *itab
data unsafe.Pointer
}
一个接口值是由两个部分组成的,即该接口对应的类型和接口对应具体的值
静态类型&动态类型
var someVariable interface{} = 101
someVariable的静态类型是interface{}, 动态类型是int
someVariable = 'Gopher'
someVariable动态类型从int变成string
类型比较
类型一致且是基本类型,值相等的时候,才能==,非基本类型会panic panic: runtime error: comparing uncomparable type []int
此处为语雀内容卡片,点击链接查看:https://www.yuque.com/albert-5xmvn/yagnte/yupka5bkgilg9dvb