东莞网站设计方案,配置网站域名,大连做网站的公司有哪些,国际重大新闻初见golang语法 package mainimport fmtfunc main() {/* 简单的程序 万能的hello world */fmt.Println(Hello Go)}
第一行代码package main定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包#xff0c;如#xff1a;package main…初见golang语法 package mainimport fmtfunc main() {/* 简单的程序 万能的hello world */fmt.Println(Hello Go)}
第一行代码package main定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包如package main。package main表示一个可独立执行的程序每个 Go 应用程序都包含一个名为 main 的包 下一行import fmt告诉 Go 编译器这个程序需要使用 fmt 包的函数或其他元素fmt 包实现了格式化 IO输入/输出的函数 下一行func main()是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的一般来说都是在启动后第一个执行的函数如果有 init() 函数则会先执行该函数
注意这里面go语言的语法定义函数的时候‘{’ 必须和函数名在同一行不能另起一行
变量声明
单独变量声明
第一种指定变量类型声明后若不赋值使用默认值0
var a int
fmt.Printf( %d\n, a)
第二种根据值自行进行判断变量类型
package mainimport (fmtreflect
)func main() {var a abcfmt.Println(a的类型是, reflect.TypeOf(a))
}第三种省略var, 注意 :左侧的变量不应该是已经声明过的否则会导致编译错误
a : 10
b : hellovar c int
c : 20 // 这里会报错因为 c 已经声明过了
总结
package mainimport fmtfunc main() {//第一种 使用默认值var a intfmt.Printf(a %d\n, a)//第二种var b int 10fmt.Printf(b %d\n, b)//第三种 省略后面的数据类型,自动匹配类型var c 20fmt.Printf(c %d\n, c)//第四种 省略var关键字d : 3.14fmt.Printf(d %f\n, d)
}
多变量声明
package mainimport fmtvar x, y int
var ( //这种分解的写法,一般用于声明全局变量a intb bool
)var c, d int 1, 2
var e, f 123, liudanbing//这种不带声明格式的只能在函数体内声明
//g, h : 123, 需要在func函数体内实现func main() {g, h : 123, 需要在func函数体内实现fmt.Println(x, y, a, b, c, d, e, f, g, h)//不能对g变量再次做初始化声明//g : 400_, value : 7, 5 //实际上7的赋值被废弃变量 _ 不具备读特性//fmt.Println(_) //_变量的是读不出来的fmt.Println(value) //5
} 常量
基本使用
常量是一个简单值的标识符在程序运行时不会被修改的量。
常量中的数据类型只可以是布尔型、数字型整数型、浮点型和复数和字符串型。
常量的定义格式 const identifier [type] value 显示定义const b string abc隐式定义const b abc
例如
package mainimport fmtfunc main() {const LENGTH int 10const WIDTH int 5 var area intconst a, b, c 1, false, str //多重赋值area LENGTH * WIDTHfmt.Printf(面积为 : %d\n, area)println(a, b, c)
}//运行结果
面积为 : 50
1 false str
常量声明枚举
在 Go 语言中没有直接的枚举类型但是我们可以可以定义一组常量来表示枚举值
const (Unknown 0Female 1Male 2
)
// 数字 0、1 和 2 分别代表未知性别、女性和男性
常量可以用len(), cap(), unsafe.Sizeof()常量计算表达式的值。常量表达式中函数必须是内置函数否则编译不过
package mainimport unsafe
const (a abcb len(a)c unsafe.Sizeof(a)
)func main(){println(a, b, c)
}
iota标识符
在我们定义一些常量的时候可能需要给它们进行赋值。在Go语言当中就可以简化这个赋值操作通过iota标示符如
package mainimport fmtconst (Sunday iotaMondayTuesdayWednesdayThursdayFridaySaturday
)func main() {today : Mondayfmt.Println(Today is:, today)
}
iota不仅仅用于自增还可以根据定义一些百搭是来进行赋值操作如
const (d iota * 2ef
)func main() {fmt.Println(d) // 0fmt.Println(e) // 2fmt.Println(f) // 4
}
函数
函数返回多个值
Go的函数可以返回多个值例如
package mainimport fmtfunc swap(x, y string) (string, string) {return y, x
}
func main() {a, b : swap(a, b)fmt.Println(a, , b)
}//执行结果b a
init函数和import
init 函数可在package main中可在其他package中可在同一个package中出现多次而main 函数只能在package main中
执行顺序
golang里面保留了两个函数init函数能应用于所有的package和main函数只能应用于package main。这两个函数在定义的时候不能有任何参数和返回值
虽然一个package里面可以写任意多个init函数但这无论是对于可读性还是以后的可维护性来说我们都强烈建议用户在一个package中每个文件只写一个init函数。go程序会自动调用init()和main()所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的但package main就必须包含一个main函数
程序的初始化和执行都起始于main包。如果main包还导入了其它的包那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入那么它只会被导入一次例如很多包可能都会用到fmt包但它只会被导入一次因为没有必要导入多次。当一个包被导入时如果该包还导入了其它的包那么会先将其它包导入进来然后再对这些包中的包级常量和变量进行初始化接着执行init函数如果有的话依次类推。等所有被导入的包都加载完毕了就会开始对main包中的包级常量和变量进行初始化然后执行main包中的init函数如果存在的话最后执行main函数。下图详细地解释了整个执行过程 代码示例
package InitLib1import fmtfunc init() {fmt.Println(lib1)
}package InitLib2import fmtfunc init() {fmt.Println(lib2)
}package mainimport (fmt_ GolangTraining/InitLib1_ GolangTraining/InitLib2
)func init() {fmt.Println(libmain init)
}func main() {fmt.Println(libmian main)
}//执行结果
lib1
lib2
libmain init
libmian main
输出的顺序与我们上面图给出的顺序是一致的。那我们现在就改动一个地方Lib1包导入Lib2main包不管
package InitLib1import (fmt_ GolangTraining/InitLib2
)func init() {fmt.Println(lib1)
}//输出结果
lib2
lib1
libmain init
libmian main
main包以及Lib1包都导入了Lib2但是只出现一次并且最先输出说明如果一个包会被多个包同时导入那么它只会被导入一次而先输出lib2是因为main包中导入Lib1时Lib1又导入了Lib2会首先初始化Lib2包的东西
注意如果需要将当前的包函数提供给外部需要将函数名首字母大写否则会引用不了
函数参数
函数的参数在传递的时候主要是分为两种类型值传递和指针传递
值传递值传递是指在调用函数时将实际参数复制一份传递到函数中这样在函数中如果对参数进行修改将不会影响到实际参数
/* 定义相互交换值的函数 */
func swap(x, y int) int {var temp inttemp x /* 保存 x 的值 */x y /* 将 y 值赋给 x */y temp /* 将 temp 值赋给 y*/return temp;
}
指针传递指针传递是指在调用函数时将实际参数的地址传递到函数中那么在函数中对参数所进行的修改将影响到实际参数
/* 定义交换值函数*/
func swap(x *int, y *int) {var temp inttemp *x /* 保持 x 地址上的值 */*x *y /* 将 y 值赋给 x */*y temp /* 将 temp 值赋给 y */
}
defer
defer类似于我们Java中的finally它会在我们程序执行的最后执行一些任务defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。
defer作用
释放占用的资源捕捉处理异常输出日志
如果一个函数中有多个defer语句它们会以LIFO后进先出的顺序执行
func Demo(){defer fmt.Println(1)defer fmt.Println(2)defer fmt.Println(3)defer fmt.Println(4)
}
func main() {Demo()
}//运行结果4 3 2 1
panic和recover
panic宕机 和 recover恢复是Go语言的两个内置函数这两个内置函数用来处理Go的运行时错误(runtime errors)。panic 用来主动抛出异常recover用来捕获panic抛出的异常
引发panic有两种情况一种是程序主动调用panic()函数另一种是程序产生运行时错误由运行时检测并抛出。例如
示例1程序主动调用panic触发宕机让程序崩溃
func funcA() {fmt.Println(func A)
}func funcB() {panic(panic in B)
}func funcC() {fmt.Println(func C)
}
func main() {funcA()funcB()funcC()
}
运行结果func A
panic: panic in Bgoroutine 1 [running]:
main.funcB(...)/home/wangxm/go_work/src/chapter05/demo.go:12
main.main()/home/wangxm/go_work/src/chapter05/demo.go:20 0x96
exit status 2
当在funcB中主动调用了panic 函数后程序发生宕机直接退出了同时输出了堆栈和goroutine相关信息这让我们可以看到错误发生的位置。当panic()触发的宕机发生时panic后面的代码将不会被执行但是在panic()函数前面的已经执行过的defer语句依然会在宕机发生时执行defer中的延迟函数。
示例2在宕机时触发defer语句延迟函数的执行
func funcA() {fmt.Println(func A)
}func funcB() {panic(panic in B)
}func funcC() {fmt.Println(func C)
}func funcD() {fmt.Println(func D)
}func main() {defer funcA()defer funcC()fmt.Println(this is main)funcB()defer funcD()
}
运行结果this is main
func C
func A
panic: panic in Bgoroutine 1 [running]:
main.funcB(...)/home/wangxm/go_work/src/chapter05/demo.go:12
main.main()/home/wangxm/go_work/src/chapter05/demo.go:29 0xca
exit status 2从运行结果可以看出当程序流程正常执行到funcB()函数的panic语句时在panic()函数被执行前defer语句会优先被执行defer语句的执行顺序是先进后出所以funcC()延迟函数先执行funcA()后执行当所有已注册的defer语句都执行完毕才会执行panic()函数触发宕机程序崩溃退出因此程序流程执行不到funcD()函数。
发生panic后程序会从调用panic的函数位置或发生panic的地方立即返回逐层向上执行函数的defer语句然后逐层打印函数调用堆栈信息直到被recover捕获或运行到最外层函数而退出。如本例中程序从funcB()函数返回到上层的main()函数中然后执行已注册的defer语句的延迟函数最后从main函数中退出并且打印了退出状态码的值为2
recover()函数用来捕获或者说是拦截panic的阻止panic继续向上层传递。无论是主动调用panic()函数触发的宕机还是程序在运行过程中由Runtime层抛出的异常都可以配合defer 和 recover 实现异常捕获和恢复让代码在发生panic后能够继续执行
Go语言没有异常系统其使用panic触发宕机类似于其他语言的抛出异常而recover的宕机恢复机制就对应try...catch机制
示例使用recover捕获panic异常恢复程序的运行
func funcA() {fmt.Println(func A)
}func funcB() {defer func(){//捕获panic并恢复程序使其继续运行if err : recover(); err ! nil {fmt.Println(recover in funcB)}}()panic(panic in B) //主动抛出异常
}func funcC() {fmt.Println(func C)
}func funcD() {fmt.Println(func D)
}func main() {defer funcA()defer funcC()fmt.Println(this is main)funcB()defer funcD()
}运行结果this is main
recover in funcB
func D
func C
func A
当recover捕获到panic时不会造成整个进程的崩溃它会从触发panic的位置退出当前函数然后继续执行后续代码
IF判断
在Go语言中if语句用于条件判断它有以下几种常见的用法和特点
基本用法
语法结构 if condition {// 当条件为真时执行的代码块
}其中condition是一个布尔表达式如果condition的值为true则执行花括号内的代码块。示例 num : 10
if num 5 {fmt.Println(num大于5)
}带有else子句
语法结构 if condition {// 当条件为真时执行的代码块
} else {// 当条件为假时执行的代码块
}示例 num : 3
if num 5 {fmt.Println(num大于5)
} else {fmt.Println(num小于等于5)
}带有else if子句
语法结构 if condition1 {// 当条件1为真时执行的代码块
} else if condition2 {// 当条件2为真时执行的代码块
} else {// 当条件1和条件2都为假时执行的代码块
}示例 num : 7
if num 10 {fmt.Println(num大于10)
} else if num 5 {fmt.Println(num大于5且小于等于10)
} else {fmt.Println(num小于等于5)
}在if语句中声明和初始化变量
语法结构 可以在if语句的条件判断部分同时声明和初始化一个变量这个变量的作用域仅限于if语句及其相关的else和else if子句 if var_declaration : expression; condition {// 当条件为真时执行的代码块且可以使用var_declaration变量
}示例 if num : 8; num 5 {fmt.Println(num大于5num的值为:, num)
}在这个例子中num在if语句中被声明和初始化并且只能在if相关的代码块中使用。如果num在if语句外已经存在那么在if语句中使用这种方式声明num会导致编译错误因为if语句中这种方式的变量声明会被视为一个新的局部变量声明
循环
在Go语言中有两种主要的循环结构for循环和range循环range可以看作是一种特殊的基于for循环的便利形式用于迭代容器类型的数据。
for循环 基本for循环 语法结构 for initialization; condition; post {// 循环体
}initialization循环开始前的初始化操作通常用于声明和初始化循环变量。condition循环的条件判断只要该条件为true循环就会继续执行。post每次循环结束后执行的操作通常用于更新循环变量。示例 for i : 0; i 5; i {fmt.Println(i)
}这个循环会从i 0开始只要i 5就会执行循环体每次循环结束后i的值会增加1最终会打印出0到4这几个数字。 range循环 用于迭代数组和切片 语法结构 for index, value : range arrayOrSlice {// 对索引和值进行处理
}其中index是数组或切片元素的索引value是元素的值。对于每个元素循环都会执行一次。示例 arr : []int{1, 2, 3, 4, 5}
for index, value : range arr {fmt.Printf(索引为 %d 的元素值为 %d\n, index, value)
}这个循环会遍历切片arr并打印出每个元素的索引和值。 用于迭代字符串 字符串在Go语言中可以看作是一个字节数组所以也可以用range循环来迭代。语法结构 for index, value : range stringValue {// 对索引和值进行处理// 需要注意的是这里的价值可能是一个字节对于ASCII字符或者是一个Unicode码点的第一个字节对于非ASCII字符// 如果要获取完整的Unicode码点需要进一步处理
}示例 str : hello
for index, value : range str {fmt.Printf(索引为 %d 的字符值为 %c\n, index, rune(value))
}这里使用rune函数将字节值转换为Unicode码点对应的字符以便正确打印出字符串中的字符。 用于迭代映射map 语法结构 for key, value : range mapValue {// 对键和值进行处理
}其中key是映射的键value是对应键的值。对于每个键值对循环都会执行一次。示例 m : map[string]int{a: 1, b: 2, c: 3}
for key, value : range m {fmt.Printf(键为 %s 的值为 %d\n, key, value)
}这个循环会遍历映射m并打印出每个键值对的键和值。
集合
集合分为slice和map两种其中slice是数组的抽象。
slice
slice是数组的抽象Go 数组的长度不可改变在特定场景中这样的集合就不太适用Go中提供了一种灵活功能强悍的内置类型切片(动态数组),与数组相比切片的长度是不固定的可以追加元素在追加时可能使切片的容量增大
方式一声明一个未指定大小的数组来定义切片
var identifier []type
方式二使用make()函数来创建切片
var slice1 []type make([]type, len)
也可以简写为
slice1 : make([]type, len)//也可以指定容量其中capacity为可选参数
make([]T, length, capacity) make函数参数说明
length 表示切片初始的长度即切片中元素的个数。这个值决定了切片创建后可以直接访问的元素范围。例如如果length为3那么可以通过切片索引0、1、2来访问元素capacity 表示切片的容量它是切片底层数组的大小。切片的容量必须大于等于长度。容量决定了切片在不重新分配内存的情况下能够扩展的最大程度。例如如果capacity为5长度为3那么在不重新分配内存的情况下切片可以通过append操作最多再容纳2个元素
操作方法
append函数
newSlice : append(slice, elements...)
其中slice是原始切片elements是要添加的一个或多个元素。append函数会返回一个新的切片这个新切片包含了原始切片的所有元素以及新添加的元素
copy函数
/* 拷贝 numbers 的内容到 numbers1 */copy(numbers1,numbers)
len函数
//获取切片长度
len(slice)
cap函数
//获取切片容量
cap(slice)
切片截取可以通过设置下限及上限来设置截取切片[lower-bound:upper-bound]
package mainimport fmtfunc main() {/* 创建切片 */numbers : []int{0, 1, 2, 3, 4, 5, 6, 7, 8}printSlice(numbers)/* 打印原始切片 */fmt.Println(numbers , numbers)/* 打印子切片从索引1(包含) 到索引4(不包含)*/fmt.Println(numbers[1:4] , numbers[1:4])/* 默认下限为 0*/fmt.Println(numbers[:3] , numbers[:3])/* 默认上限为 len(s)*/fmt.Println(numbers[4:] , numbers[4:])numbers1 : make([]int, 0, 5)printSlice(numbers1)/* 打印子切片从索引 0(包含) 到索引 2(不包含) */number2 : numbers[:2]printSlice(number2)/* 打印子切片从索引 2(包含) 到索引 5(不包含) */number3 : numbers[2:5]printSlice(number3)}func printSlice(x []int) {fmt.Printf(len%d cap%d slice%v\n, len(x), cap(x), x)
}运行结果
len9 cap9 slice[0 1 2 3 4 5 6 7 8]
numbers [0 1 2 3 4 5 6 7 8]
numbers[1:4] [1 2 3]
numbers[:3] [0 1 2]
numbers[4:] [4 5 6 7 8]
len0 cap5 slice[]
len2 cap9 slice[0 1]
len3 cap7 slice[2 3 4]
map
映射map是Go语言中一种用于存储键值对的数据结构。以下是一些常见的map操作方法
1. 创建map
可以使用make函数创建map语法为make(map[KeyType]ValueType)其中KeyType是键的类型ValueType是值的类型。例如m : make(map[string]int)创建了一个键为字符串类型值为整数类型的map。
2. 插入键值对
可以直接给map赋值来插入键值对。例如m : make(map[string]int); m[key1] 1这里将键为key1值为1的键值对插入到map中。
3. 访问键值对
使用键来访问map中的值。例如m : make(map[string]int); m[key1] 1; fmt.Println(m[key1])会打印出键为key1的值1。如果访问的键不存在于map中会返回值类型的零值。例如m : make(map[string]int); fmt.Println(m[nonexistent_key])会打印出整数类型的零值0。
4. 修改键值对
可以使用键来修改map中的值。例如m : make(map[string]int); m[key1] 1; m[key1] 2; fmt.Println(m[key1])会将键为key1的值从1修改为2。
5. 删除键值对
使用delete函数来删除键值对语法为delete(map, key)其中map是要操作的mapkey是要删除的键。例如m : make(map[string]int); m[key1] 1; delete(m, key1); fmt.Println(m[key1])会删除键为key1的值并返回整数类型的零值0因为该键值对已经被删除。
6. 检查键是否存在
可以使用一种特殊的语法来检查键是否存在于map中同时获取对应的值。例如m : make(map[string]int); m[key1] 1; value, exists : m[key1]; fmt.Println(value, exists)会打印出1 true因为键key1存在于map中并且对应的值为1。如果键不存在exists会返回falsevalue会返回值类型的零值。
7. 遍历map
使用range来遍历map会依次获取map中的键值对。例如m : make(map[string]int); m[key1] 1; m[ive had no joy with maps in GoLang!: Here is an example of a text that could be inserted into a map, though it might not be a very practical one]; for key, value : range m { fmt.Println(key, value) }会遍历map中的键值对并打印出来。
8. map的长度
使用len函数可以获取map的长度即map中键值对的个数。例如m : make(map[string]int); m[key1] 1; m[key2] 2; fmt.Println(len(m))会打印出2因为这个map中有两个键值对
结构体
在 Go 语言中结构体struct是一种复合数据类型它允许用户将不同类型的数据组合在一起形成一个新的类型。以下是关于结构体的详细介绍
结构体的定义
结构体的定义一个是使用struct关键字基础语法如下
type StructName struct {Field1 Type1Field2 Type2//...
}
其中type是关键字用于定义新的类型StructName是结构体的名称struct是结构体的关键字后面跟着大括号大括号内是结构体的各个字段field每个字段由字段名和字段类型组成。例如
type Person struct {Name stringAge int
}定义了一个名为Person的结构体它包含两个字段Name字符串类型和Age整数类型
结构体变量的创建和初始化
方法一逐个字段初始化 可以通过结构体类型创建结构体变量并逐个初始化结构体的字段。例如
var p Person
p.Name Alice
p.Age 25方法二使用结构体字面量初始化 使用结构体字面量可以在创建结构体变量的同时初始化结构体的字段。有两种形式按字段顺序初始化
p : Person{Bob, 30}这种方式要求按照结构体定义时字段的顺序提供值。
指定字段名初始化
p : Person{Name: Charlie, Age: 35}这种方式可以不按照结构体定义时字段的顺序提供值只要指定正确的字段名和值即可。
结构体的嵌套
结构体可以嵌套其他结构体。例如
type Address struct {City stringStreet stringZipCode int
}type Employee struct {PersonJobTitle stringAddress AddressSalary int
}这里定义了Address结构体和Employee结构体Employee结构体嵌套了Person结构体和Address结构体。当访问嵌套结构体的字段时可以使用.操作符进行多级访问。例如e : Employee{Person{David, 40}, Engineer, Address{New York, 5th Avenue, 10001}, 80000}; fmt.Println(e.Person.Name)会打印出David。
结构体方法
可以为结构体定义方法结构体方法类似于面向对象语言中的类方法。结构体方法的定义语法如下
func (s StructName) MethodName() ReturnType {// 方法体
}其中(s StructName)部分称为接收者receiver它表示该方法是属于StructName结构体的s是接收者变量可以在方法体中使用接收者变量来访问结构体的相关字段。例如
func (p Person) GetName() string {return p.Name
}定义了一个Person结构体的方法GetName它返回Person结构体的Name字段的值。
结构体的内存布局
结构体在内存中的布局是按照字段定义的顺序依次排列的每个字段占用一定的内存空间。不同类型的字段在内存中所占的空间大小不同例如整数类型通常占用4个字节在32位系统中或8个字节在62位系统中字符串类型的内存占用则比较复杂它包含一个指针和一些其他信息。当结构体嵌套其他结构体时嵌套的结构体的内存空间也是按照其自身的字段定义顺序依次排列在主结构体的内存空间内。
结构体的比较
如果结构体的所有字段都是可比较的那么结构体本身也是可比较的。比较两个结构体时会逐个比较结构体的各个字段。例如
p1 : Person{Alice, 25}
p2 : Person{Alice, 25}
if p1 p2 {fmt.Println(p1和p2相等)
}如果结构体中有不可比较的字段如切片、映射等那么结构体本身不可比较
接口
接口定义
首先我们定义了一个接口Shape
type Shape interface {Area() float64
}这个接口规定了任何实现它的类型都必须有一个Area方法该方法没有参数并且返回一个float64类型的值。这就像是一个合同规定了实现这个接口的类型需要具备计算面积的能力。
接口实现
然后我们有一个Rectangle结构体
type Rectangle struct {length float64width float64
}并且为这个结构体定义了一个Area方法
func (r Rectangle) Area() float64 {return r.length * r.width
}通过定义这个Area方法Rectangle结构体满足了Shape接口的要求也就是说Rectangle结构体实现了Shape接口。这就好比Rectangle结构体签署了Shape接口这个合同它具备了按照合同要求计算面积的能力。
接口使用
作为函数参数我们定义了一个函数CalculateArea
func CalculateArea(s Shape) float64 {return s.Area()
}在这个函数中参数s是Shape接口类型。这意味着我们可以传入任何实现了Shape接口的类型的值作为参数。当我们调用这个函数时如果传入的是Rectangle结构体的值因为Rectangle实现了Shape接口那么在函数内部就会调用Rectangle结构体的Area方法来计算面积。
作为变量类型我们还可以定义一个Shape接口类型的变量s
var s Shape
r : Rectangle{length: 5, width: 3}
s r
fmt.Println(s.Area())这里首先定义了一个Shape接口类型的变量s然后创建了一个Rectangle结构体的值r并将r赋值给s。因为Rectangle实现了Shape接口所以这种赋值是合法的。最后我们可以通过s调用Area方法来计算面积实际上是调用了Rectangle结构体的Area方法。
这样通过接口我们可以实现不同类型之间的通用性和多态性使得代码更加灵活和可维护。例如如果我们还有一个Square结构体也实现了Shape接口那么我们可以同样使用CalculateArea函数来计算它的面积而不需要为Square重新定义一个计算面积的函数。
面向对象的特性
Go 语言虽然不是纯粹的面向对象编程语言但它支持一些面向对象的特性主要包括封装、继承和多态以下是具体介绍
封装性
定义封装是指将数据和操作数据的方法组合在一起并对外部隐藏数据的实现细节只提供必要的接口来访问和操作数据
实现方式通过结构体和方法实现结构体用于定义数据结构将相关的数据组合在一起。例如定义一个Rectangle结构体来表示矩形的长和宽
type Rectangle struct {length float64width float64
}方法用于操作结构体中的数据。例如为Rectangle结构体定义一个计算面积的方法
func (r Rectangle) Area() float64 {return r.length * r.width
}通过这种方式Rectangle结构体的内部数据长和宽被封装起来外部只能通过Area方法来获取矩形的面积而无法直接访问长和宽的数据
看下面的例子会更加清晰
package mainimport fmt//定义一个结构体
type T struct {name string
}func (t T) method1() {t.name new name1
}func (t *T) method2() {t.name new name2
}func main() {t : T{old name}fmt.Println(method1 调用前 , t.name)t.method1()fmt.Println(method1 调用后 , t.name)fmt.Println(method2 调用前 , t.name)t.method2()fmt.Println(method2 调用后 , t.name)
}
运行结果
method1 调用前 old name
method1 调用后 old name
method2 调用前 old name
method2 调用后 new name2
当调用t.method1()时相当于method1(t)实参和行参都是类型 T可以接受。此时在method1()中的t只是参数t的值拷贝所以method1()的修改影响不到main中的t变量。
当调用t.method2()method2(t)这是将 T 类型传给了 *T 类型go可能会取 t 的地址传进去method2(t)。所以 method1() 的修改可以影响 t。
所以说下面的代码保证了封装性
func (t T) method1() {t.name new name1
}继承
在Go语言中虽然没有像传统面向对象语言中那样的类继承机制但可以通过结构体嵌套实现类似继承的效果。以下是详细说明
基本概念
继承是一种机制它允许一个类型子类或派生类继承另一个类型父类或基类的属性和方法从而实现代码的复用和扩展。子类可以在继承父类的基础上添加自己的特性并且可以重写父类的某些方法以满足自身的需求。
Go语言中的实现方式 - 结构体嵌套
假设我们有一个Base结构体代表基类它具有一些属性和方法。例如
type Base struct {Property1 stringProperty2 int
}func (b Base) Method1() {// 基类方法的实现
}现在我们想要创建一个Derived结构体来继承Base结构体的特性。我们可以通过结构体嵌套来实现
type Derived struct {BaseAdditionalProperty string
}通过这种嵌套方式Derived结构体继承了Base结构体的所有属性和方法。例如Derived结构体可以直接访问Base结构体的Property1和Property2属性并且可以调用Base结构体的Method1方法。
方法重写覆盖
在Go语言中虽然没有严格的方法重写语法但通过结构体嵌套和方法定义可以实现类似的效果。假设我们想要在Derived结构体中重写Base结构体的Method1方法。我们可以在Derived结构体中重新定义一个Method1方法
func (d Derived) Method1() {// 新的方法实现可能会调用原始Base结构体的方法或者完全替代它// 例如可以先调用原始Base结构体的方法然后再添加一些额外的操作d.Base.Method1()// 其他额外操作
}这样当我们调用Derived结构体的Method1方法时执行的是我们在Derived结构体中重新定义的方法而不是Base结构体的原始方法。
注意事项
虽然结构体嵌套实现了类似继承的功能但它与传统的继承机制还是有一些区别。例如在Go语言中没有像其他语言中那样的类层次结构和多态性的严格语法定义。这种结构体嵌套的方式在实际应用中需要谨慎使用要确保代码的可读性和可维护性。如果嵌套过于复杂可能会导致代码难以理解和维护。
多态
定义多态是指不同类型的对象对同一方法调用可以表现出不同的行为。
实现方式通过接口实现多态首先定义一个接口接口中规定了一些方法但没有具体的实现。例如定义一个Shape接口要求实现Area方法
type Shape interface {Area() float64
}然后让不同的结构体实现这个接口。例如Rectangle和Square结构体都可以实现Shape接口
func (r Rectangle) Area() float64 {return r.length * r.width
}
func (s Square) Area() float64 {return s.sideLength * s.sideLength
}
当使用接口类型的变量时根据实际赋值的结构体不同调用Area方法会表现出不同的行为。例如
var s Shape
r : Rectangle{length: 5, width: 3}
s r
fmt.Println(s.Area()) // 输出15
s1 : Square{sideLength: 4}
s s1
fmt.Println(s.Area()) // 输出16
通过接口实现了多态使得程序可以根据不同的对象类型灵活地执行相应的操作。