46.结构struct
Go 中的struct与C中的struct非常相似,并且Go没有class
使用 type <Name> struct{} 定义结构,名称遵循可见性规则
package main
import "fmt"
type persion struct {
Name string
Age int
}
func main() {
a := persion{}
a.Name="david"
a.Age=13
fmt.Println(a)
}
{david 13}
package main
import "fmt"
type persion struct {
Name string
Age int
}
func main() {
a := persion{Name:"david"}
fmt.Println(a)
}
{david 0} //0是int的初始值
package main
import "fmt"
type persion struct {
Name string
Age int
}
func main() {
a := persion{Name:"david",Age:13}
fmt.Println(a)
}
{david 13}
package main
import "fmt"
type persion struct {
Name string
Age int
}
func main() {
a := persion{"david",28}
fmt.Println(a)
}
{david 28}
struct也是一个值类型,传递的时候也是值拷贝
package main
import "fmt"
type persion struct {
Name string
Age int
}
func main() {
a := persion{
Name:"david",
Age:28,
}
fmt.Println(a)
A(a)
fmt.Println(a)
}
func A(per persion){
per.Age = 13
fmt.Println("A",per)
}
{david 28}
A {david 13}
{david 28}
支持指向自身的指针类型成员
如何真正修改Age为13呢?答案是采用指针方式修改内存地址中的值,指针值传递
package main
import "fmt"
type persion struct {
Name string
Age int
}
func main() {
a := persion{
Name:"david",
Age:28,
}
fmt.Println(a)
A(&a)
fmt.Println(a)
}
func A(per *persion){
per.Age = 13
fmt.Println("A",per)
}
{david 28}
A &{david 13}
{david 13}//这里修改了内存中的age为13
package main
import "fmt"
type persion struct {
Name string
Age int
}
func main() {
a := persion{
Name:"david",
Age:28,
}
fmt.Println(a)
A(&a)
B(&a)
fmt.Println(a)
}
func A(per *persion){
per.Age = 13
fmt.Println("A",per)
}
func B(per *persion){
per.Age = 15
fmt.Println("B",per)
}
{david 28}
A &{david 13}
B &{david 15}
{david 15}//如果有一个B函数,修改age为15,最终age等于15
如果有(100个)多个函数需要调用?每次都需要取地址符号,很麻烦,怎么办? 用指针保存,把a变为一个指向结构的指针呢?初始化的时候就把地址取出来
package main
import "fmt"
type persion struct {
Name string
Age int
}
func main() {
a := &persion{ //定义a是一个指针
Name:"david",
Age:28,
}
fmt.Println(a)
A(a) //这里直接调用即可,不用取地址
B(a) //这里直接调用即可,不用取地址
fmt.Println(a)
}
func A(per *persion){
per.Age = 23
fmt.Println("A",per)
}
func B(per *persion){
per.Age = 25
fmt.Println("B",per)
}
&{david 28}//这里可以看到a是一个指向struct的指针
A &{david 23}
B &{david 25}
&{david 25}
如果此时我需要要像class一样对a的属性性操作,怎么做?a.Name = "ok"
package main
import "fmt"
type persion struct {
Name string
Age int
}
func main() {
a := &persion{"david", 28,}
a.Name="ok" //这里直接修改属性的值
fmt.Println(a)
A(a)
fmt.Println(a)
}
func A(per *persion) {
per.Age = 23
fmt.Println("A", per)
}
&{ok 28}
A &{ok 23}
&{ok 23}
支持匿名结构,可用作成员或定义成员变量
package main
import "fmt"
func main() {
a := struct{
Name string
Age int
}{
Name:"david",
Age:19,
}
fmt.Println(a)
}
{david 19}
也可以定义指针类型的匿名结构
package main
import "fmt"
func main() {
a := &struct{ //a是没有名称的匿名结构,定义指针类型结构
Name string
Age int
}{ //需要对结构属性(字面值)赋值
Name:"david",
Age:19,
}
fmt.Println(a)
}
&{david 19}
匿名结构是否可以嵌套其他结构中呢?可以
package main
import "fmt"
type person struct {
Name string
Age int
Contact struct {
Phone,City string
}
}
func main() {
a := person{}
fmt.Println(a)
}
{ 0 { }} //0是int型的Age初始值,{ }是Contact的结构
package main
import "fmt"
type person struct {
Name string
Age int
Contact struct {
Phone,City string
}
}
func main() {
a := person{Name:"david",Age:13}
a.Contact.City="sahgnhai"//匿名结构赋值的方法
a.Contact.Phone="111111111"
fmt.Println(a)
}
{david 13 {111111111 sahgnhai}}
什么是匿名字段?
package main
import "fmt"
type person struct {
string //这就是匿名字段,没有定义属性的名字,只是定义了类型
int
}
func main() {
a := person{"david",19}//注意,赋值顺序是string、int,不能对调,否则报错
fmt.Println(a)
}
{david 19}
匿名结构也可以用于map的值
可以使用字面值对结构进行初始化
允许直接通过指针来读写结构成员
相同类型的成员可进行直接拷贝赋值
支持 == 与 !=比较运算符,但不支持 > 或 <
支持匿名字段,本质上是定义了以某个类型名为名称的字段
结构也是一种类型,相同的类型之间,变量可以进行赋值
package main
import "fmt"
type person struct {
Name string
Age int
}
func main() {
a := person{"david",19}
var b person
b=a //把a赋值给b,打印出来b和a是一样的
fmt.Println(b)
}
{david 19}
struce是一种类型,所以可以比较?
a和b虽然内容包含相同,但是名称不同,就是不同类型,没有可比性,无法比较,所以报错,只有用相同的struct person才可以比较。
package main
import "fmt"
type person1 struct {
Name string
Age int
}
type person2 struct {
Name string
Age int
}
func main() {
a := person1{"david",19}
b := person2{"david",19}
fmt.Println(a == b )
}
invalid operation: a == b (mismatched types person1 and person2)
package main
import "fmt"
type person struct {
Name string
Age int
}
func main() {
a := person{"david",19}
b := person{"david",19}
fmt.Println(a == b )
}
true
package main
import "fmt"
type person struct {
Name string
Age int
}
func main() {
a := person{"david",19}
b := person{"david",20}
fmt.Println(a == b )
}
false
嵌入结构作为匿名字段看起来像继承,但不是继承
可以使用匿名字段指针
package main
import (
"fmt"
)
type human struct {
Sex int
}
type teacher struct {
human
Name string
Age int
}
type student struct {
human
Name string
Age int
}
func main() {
a := teacher{Name:"joe",Age:19}
b := student{Name:"joe",Age:20}
fmt.Println(a,b )
}
{{0} joe 19} {{0} joe 20} //human的int默认值0已经嵌套进去了
package main
import (
"fmt"
)
type human struct {
Sex int
}
type teacher struct {
human
Name string
Age int
}
type student struct {
human
Name string
Age int
}
func main() {
a := teacher{Name:"joe",Age:19,Sex:0}
b := student{Name:"joe",Age:20,Sex:1}
fmt.Println(a,b )
}
hello.go:19:36: unknown field 'Sex' in struct literal of type teacher
hello.go:20:36: unknown field 'Sex' in struct literal of type student
//这里报错了,因为初始化方法错误,应该怎么定义呢?
方法1:human在teacher中被当做匿名字段,所以定义human:{Sex:0}
package main
import (
"fmt"
)
type human struct {
Sex int
}
type teacher struct {
human
Name string
Age int
}
type student struct {
human
Name string
Age int
}
func main() {
a := teacher{Name:"joe",Age:19,human:human{Sex:0}}//human在teacher中被当做匿名字段变量,给变量赋值
b := student{Name:"joe",Age:20,human:human{Sex:1}}
fmt.Println(a,b )
}
{{0} joe 19} {{1} joe 20}
package main
import (
"fmt"
)
type human struct {
Sex int
}
type teacher struct {
human
Name string
Age int
}
type student struct {
human
Name string
Age int
}
func main() {
a := teacher{Name:"joe",Age:19,human:human{Sex:0}}
b := student{Name:"joe",Age:20,human:human{Sex:1}}
a.Name="joe2"
a.Age=13
a.human.Sex=100 //标准方法,防止有多个导入字段重复报错
a.Sex=100 //sex已经被当做teacher的一个属性嵌入,human结构嵌入,把嵌入结构的字段sex都给了外层结构teacher
fmt.Println(a,b )
}
{{100} joe2 13} {{1} joe 20}
如果匿名字段和外层结构有同名字段,应该如何进行操作?
package main
import (
"fmt"
)
type A struct {
B
Name string
}
type B struct {
Name string
}
func main() {
a := A{Name: "A", B: B{Name: "B"}}
fmt.Println(a.Name,a.B.Name) //分别输出a和b的name值A和B
}
A B
如果A中不存在Name字段,那么打印a.Name是什么呢?答案是B
package main
import (
"fmt"
)
type A struct {
B
}
type B struct {
Name string
}
func main() {
a := A{B: B{Name: "B"}}
fmt.Println(a.Name,a.B.Name)//此时的a.Name=B,此时a.Name,a.B.Name写法等价
}
B B
package main
import (
"fmt"
)
type A struct {
B
C
}
type B struct {
Name string
}
type C struct {
Name string
}
func main() {
a := A{B: B{Name: "B"},C: C{Name: "C"}}
fmt.Println(a.Name,a.B.Name,a.C.Name)
}
ambiguous selector a.Name//报错提示有重名的字段,因为a.Name此时到底等于a.B.Name还是等于a.C.Name呢?完全不清楚
47.方法method
Go 中虽没有class,但依旧有method
只能为同一个包中的类型结构定义方法method呢?通过显示说明receiver来实现与某个类型的组合,编译器根据接受者的类型来判断是哪一个类型的方法
package main
import (
"fmt"
)
type A struct {
Name string
}
type B struct {
Name string
}
//定义一个链接到struct A的方法print
func(a A)Print(){ //局部变量a的接受者类型是struct:A,所以定义的方法print是struct A的类型方法
fmt.Println("A")
}
func main(){
a :=A{} //声明一个结构A
a.Print()//然后a调用Print方法,打印结果A
}
A
package main
import (
"fmt"
)
type A struct {
Name string
}
type B struct {
Name string
}
//定义一个链接到struct A的方法print
func(a A)Print(){ //局部变量a的接受者类型是struct:A,所以定义的方法print是struct A的类型方法
a.Name = "A"
fmt.Println("A")
}
func(b B)Print(){ //定义一个连接到B结构的方法Print
b.Name="B"
fmt.Println("B")
}
func main(){
c :=A{} //声明一个结构A
c.Print()//然后a调用Print方法,打印结果A
d :=B{}
d.Print()//注意这里调用方法使用c.Print()和d.Print(),不能使用Print(),否则编译器不清楚是c还是d的Print()方法
}
A
B
Receiver 可以是类型的值或者指针
package main
import (
"fmt"
)
type A struct {
Name string
}
type B struct {
Name string
}
//定义一个链接到struct A的指针类型的方法Print
func(a *A)Print(){ //局部变量a的接受者类型是指针类型struct:A,所以定义的方法print是指针类型struct A
a.Name = "AAA" //引用类型是指针类型拷贝,操作了内存中对象,所以a.Name = "AAA"保存到内存中
fmt.Println("A")
}
func(b B)Print(){
b.Name="BB" //值类型以值传递,只是得到一个拷贝副本,结束方法之后失效,打印空
fmt.Println("B")
}
func main(){
a :=A{} //声明一个结构A
a.Print()//然后a调用Print方法,打印结果A
fmt.Println(a.Name)
b :=B{}
b.Print()
fmt.Println(b.Name)
}
A
AAA
B
不存在方法重载
可以使用值或指针来调用方法,编译器会自动完成转换
package main
import (
"fmt"
)
type TZ int //可以为任何类型绑定方法Print(),非常灵活,可以对int类型做高级的绑定定义等
func (a *TZ)Print(){
fmt.Println("TZ")
}
func main(){
var a TZ
a.Print()
}
TZ //这里打印TZ
从某种意义上来说,方法是函数的语法,因为receiver其实就是
方法所接收的第1个参数(Method Value vs. Method Expression)
package main
import (
"fmt"
)
type TZ int
func (a *TZ)Print(){
fmt.Println("TZ")
}
func main(){
var a TZ
a.Print()//Method Value,已经声明了receiver a,通过类似类的方法调用的形式
(*TZ).Print(&a)//Method Expression,通过类型(*TZ),把变量&a作为receiver(第一个参数)传给对应的Print方法,而不是类型变量调用方法
}
TZ
TZ
如果外部结构和嵌入结构存在同名方法,则优先调用外部结构的方法
类型别名不会拥有底层类型所附带的方法
方法访问权限的问题:
方法可以调用结构中的非公开字段,同一包package中方法访问权限认为公开的,不是私有的,相对其他包package才认为是私有字段:
package main
import (
"fmt"
)
type A struct {
name string
}
func (a *A)Print(){
a.name = "123"
fmt.Println(a.name)
}
func main() {
a:=A{}
a.Print()
fmt.Println(a.name)
}
输出:
123
123
根据为结构增加方法的知识,尝试声明一个底层类型为int的类型,
并实现调用某个方法就递增100。0+100=100
如:a:=0,调用a.Increase()之后,a从0变成100。
package main
import (
"fmt"
)
type TZ int
func (tz *TZ)Increase(num int){
*tz += TZ(num)//这里需要经num强制类型转换为TZ型,否则报错,因为TZ和int是不同的类型
}
func main() {
var a TZ //声明a是TZ类型,a的初始值是0
a.Increase(100)
fmt.Println(a)
}
100
48.接口interface
接口是一个或多个方法签名的集合
只要某个类型拥有该接口的所有方法签名,即算实现该接口,无需显示声明实现了哪个接口,这称为 Structural Typing
package main
import (
"fmt"
)
type USB interface{ //定义一个USB接口
Name() string //返回USB名称
Connect() //连接的方法
}
type PhoneConnecter struct{ //定义手机连接器结构,有一个name变量
name string
}
//怎么样用结构让USB实现呢?实现就是为结构添加方法,对应USB中的Name()
func (pc PhoneConnecter)Name() string { //定义一个receiver是PhoneConnecter结构的Name()方法,返回一个string类型的pc.name
return pc.name
}
//怎么样用结构让USB实现呢?实现就是为结构添加方法,对应USB中的Connect()
func (pc PhoneConnecter)Connect(){ //定义一个receiver是PhoneConnecter结构的Connect()方法,打印pc.name
fmt.Println("Connect",pc.name)
}
func main(){
var a USB //定义一个USB接口
a = PhoneConnecter{"PhoneConnecter"}//初始化一个name=PhoneConnecter的手机连接器(具有接口USB属性)
a.Connect()
}
Connect PhoneConnecter
package main
import (
"fmt"
)
type USB interface{ //定义一个USB接口
Name() string //返回USB名称
Connect() //连接的方法
}
type PhoneConnecter struct{ //定义手机连接器结构,有一个name变量
name string
}
//怎么样用结构让USB实现呢?实现就是为结构添加方法,对应USB中的Name()
func (pc PhoneConnecter)Name() string { //定义一个receiver是PhoneConnecter结构的Name()方法,返回一个string类型的pc.name
return pc.name
}
//怎么样用结构让USB实现呢?实现就是为结构添加方法,对应USB中的Connect()
func (pc PhoneConnecter)Connect(){ //定义一个receiver是PhoneConnecter结构的Connect()方法,打印pc.name
fmt.Println("Connect",pc.name)
}
func Disconnect(usb USB){
fmt.Println("Disconnected")
}
func main(){
//var a USB 这里省略定义一个USB接口,因为PhoneConnecter已经具备了USB的Name属性和Connect方法,就已经实现了USB接口,可以直接调用,也可以Disconnect
a := PhoneConnecter{"PhoneConnecter"}//初始化一个name=PhoneConnecter的手机连接器(具有接口USB属性)
a.Connect()
Disconnect(a)
}
Connect PhoneConnecter
Disconnected
接口只有方法声明,没有实现,没有数据字段
接口可以匿名嵌入其它接口,或嵌入到结构中
package main
import (
"fmt"
)
type USB interface{ //定义一个USB接口
Name() string //返回USB名称
Connecter //连接的方法
}
type Connecter interface{ //这里的Connecter具备Connect()方法,所以USB中直接嵌入Connecter
Connect()
}
type PhoneConnecter struct{ //定义手机连接器结构,有一个name变量
name string
}
//怎么样用结构让USB实现呢?实现就是为结构添加方法,对应USB中的Name()
func (pc PhoneConnecter)Name() string { //定义一个receiver是PhoneConnecter结构的Name()方法,返回一个string类型的pc.name
return pc.name
}
//怎么样用结构让USB实现呢?实现就是为结构添加方法,对应USB中的Connect()
func (pc PhoneConnecter)Connect(){ //定义一个receiver是PhoneConnecter结构的Connect()方法,打印pc.name
fmt.Println("Connect",pc.name)
}
func Disconnect(usb USB){
fmt.Println("Disconnected")
}
func main(){
//var a USB //定义一个USB接口
a := PhoneConnecter{"PhoneConnecter"}//初始化一个name=PhoneConnecter的手机连接器(具有接口USB属性)
a.Connect()
Disconnect(a)
}
Connect PhoneConnecter
Disconnected
package main
import (
"fmt"
)
type USB interface{ //定义一个USB接口
Name() string //返回USB名称
Connecter //连接的方法
}
type Connecter interface{ //这里的Connecter具备Connect()方法,所以USB中直接嵌入Connecter
Connect()
}
type PhoneConnecter struct{ //定义手机连接器结构,有一个name变量
name string
}
func (pc PhoneConnecter)Name() string { //定义一个receiver是PhoneConnecter结构的Name()方法,返回一个string类型的pc.name
return pc.name
}
func (pc PhoneConnecter)Connect(){ //定义一个receiver是PhoneConnecter结构的Connect()方法,打印pc.name
fmt.Println("Connect",pc.name)
}
//这里只是单纯的disconnected,但是不知道谁断开了连接,不知道断开的是不是一个PhoneConnecter,怎么办呢?
func Disconnect(usb USB){
if pc,ok :=usb.(PhoneConnecter);ok { //ok,pattern模式,类型判断USB是否是PhoneConnecter结构?如果成立ok=true,打印pc.name
fmt.Println("Disconnected",pc.name)
return
}
fmt.Println("Unknow device")
}
func main(){
//var a USB //定义一个USB接口
a := PhoneConnecter{"PhoneConnecter"}//初始化一个name=PhoneConnecter的手机连接器(具有接口USB属性)
a.Connect()
Disconnect(a)
}
Connect PhoneConnecter
Disconnected PhoneConnecter
49.类型断言
通过类型断言的ok pattern可以判断接口中的数据类型
使用type switch则可针对空接口进行比较全面的类型判断
package main
import (
"fmt"
)
type empty interface{
}
type USB interface{ //定义一个USB接口
Name() string //返回USB名称
Connecter //连接的方法
}
type Connecter interface{ //这里的Connecter具备Connect()方法,所以USB中直接嵌入Connecter
Connect()
}
type PhoneConnecter struct{ //定义手机连接器结构,有一个name变量
name string
}
func (pc PhoneConnecter)Name() string { //定义一个receiver是PhoneConnecter结构的Name()方法,返回一个string类型的pc.name
return pc.name
}
func (pc PhoneConnecter)Connect(){ //定义一个receiver是PhoneConnecter结构的Connect()方法,打印pc.name
fmt.Println("Connect",pc.name)
}
func Disconnect(usb interface{}){
switch v := usb.(type){ //这里使用高效简便的type switch方法判断局部变量v的类型
case PhoneConnecter:
fmt.Println("Disconnected",v.name)
default:
fmt.Println("Unknown device")
}
}
func main(){
//var a USB //定义一个USB接口
a := PhoneConnecter{"PhoneConnecter"}//初始化一个name=PhoneConnecter的手机连接器(具有接口USB属性)
a.Connect()
Disconnect(a)
}
Connect PhoneConnecter
Disconnected PhoneConnecter
50.接口转换
可以将拥有超集的接口转换为子集的接口
可以把USB转换为Connecter方法,因为USB还包含了name属性
package main
import (
"fmt"
)
type USB interface{ //定义一个USB接口
Name() string //返回USB名称
Connecter //连接的方法
}
type Connecter interface{ //这里的Connecter具备Connect()方法,所以USB中直接嵌入Connecter
Connect()
}
type PhoneConnecter struct{ //定义手机连接器结构,有一个name变量
name string
}
func (pc PhoneConnecter)Name() string { //定义一个receiver是PhoneConnecter结构的Name()方法,返回一个string类型的pc.name
return pc.name
}
func (pc PhoneConnecter)Connect(){ //定义一个receiver是PhoneConnecter结构的Connect()方法,打印pc.name
fmt.Println("Connect",pc.name)
}
func main(){
b := PhoneConnecter{"PhoneConnecter"}//初始化一个name=PhoneConnecter的手机连接器b(天生实现了接口USB接口),因为b具备Name属性和Connect()方法
b.Connect()
fmt.Println(b.name)
var a Connecter
a=Connecter(b)//强制类型转换,将PhoneConnecter的pc转换为Connecter,connect只有Connect()方法,没有name属性
a.Connect()
//fmt.Println(a.name) 这里打印出错,因为Connecter没有name属性
}
Connect PhoneConnecter
PhoneConnecter
Connect PhoneConnecter
将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针
package main
import (
"fmt"
)
type USB interface{ //定义一个USB接口
Name() string //返回USB名称
Connecter //连接的方法
}
type Connecter interface{ //这里的Connecter具备Connect()方法,所以USB中直接嵌入Connecter
Connect()
}
type PhoneConnecter struct{ //定义手机连接器结构,有一个name变量
name string
}
func (pc PhoneConnecter)Name() string { //定义一个receiver是PhoneConnecter结构的Name()方法,返回一个string类型的pc.name
return pc.name
}
func (pc PhoneConnecter)Connect(){ //定义一个receiver是PhoneConnecter结构的Connect()方法,打印pc.name
fmt.Println("Connect",pc.name)
}
func main(){
b := PhoneConnecter{"PhoneConnecter"}//初始化一个name=PhoneConnecter的手机连接器b(天生实现了接口USB接口),因为b具备Name属性和Connect()方法
var a Connecter
a=Connecter(b)//强制类型转换,将PhoneConnecter的pc转换为Connecter,connect只有Connect()方法,没有name属性
a.Connect()
b.name="pc"
a.Connect() //这里完全忽视了我们的修改,因为拿到的是一个拷贝
}
Connect PhoneConnecter
Connect PhoneConnecter
只有当接口存储的类型和对象都为nil时,接口才等于nil
package main
import "fmt"
func main(){
var a interface{} //a=nil
fmt.Println(a==nil)
var p *int = nil
a = p //a指向的对象是nil,但是本身是一个指向int型的指针,不是nil,所以打印false
fmt.Println(a==nil)
}
true
false
接口调用不会做receiver的自动转换
接口同样支持匿名字段方法
接口也可实现类似OOP中的多态
空接口可以作为任何类型数据的容器
51.反射reflection
反射可大大提高程序的灵活性,使得 interface{} 有更大的发挥余地
反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
反射会将匿名字段作为独立字段(匿名字段本质)
想要利用反射修改对象状态,前提是 interface.data 是 settable,
即 pointer-interface
通过反射可以“动态”调用方法
定义一个结构,通过反射来打印其信息,并调用方法。
52.并发concurrency
很多人都是冲着 Go 大肆宣扬的高并发而忍不住跃跃欲试,但其实从
源码的解析来看,goroutine 只是由官方实现的超级“线程池”而已。
不过话说回来,每个实例 4-5KB 的栈内存占用和由于实现机制而大幅
减少的创建和销毁开销,是制造 Go 号称的高并发的根本原因。另外,
goroutine 的简单易用,也在语言层面上给予了开发者巨大的便利。
并发不是并行:Concurrency Is Not Parallelism
并发主要由切换时间片来实现“同时”运行,在并行则是直接利用
多核实现多线程的运行,但 Go 可以设置使用核数,以发挥多核计算机
的能力。
Goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
53.Channel
Channel 是 goroutine 沟通的桥梁,大都是阻塞同步的
通过 make 创建,close 关闭
Channel 是引用类型
可以使用 for range 来迭代不断操作 channel
可以设置单向或双向通道
可以设置缓存大小,在未被填满前不会发生阻塞
54.Select
可处理一个或多个 channel 的发送与接收
同时有多个可用的 channel时按随机顺序处理
可用空的 select 来阻塞 main 函数
可设置超时
创建一个 goroutine,与主线程按顺序相互发送信息若干次并打印
有疑问加站长微信联系(非本文作者)