golang函数
什么是方法
一个方法只是一个函数,它有一个特殊的接收者(receiver)类型,该接收者放在func
关键字和函数名之间。接收者可以是结构体类型或非结构体类型。可以在方法内部访问接收者。
通过下面的语法创建一个方法:
func(tType)methodName(parameterlist){ }12
上面的代码片段创建了一个名为methodName
的方法,该方法有一个类型为Type
的接收者。
例子
让我们编写一个简单的程序,它创建一个结构体类型的方法并调用它。
packagemain import( "fmt" ) typeEmployeestruct{ namestring salaryint currencystring } /* displaySalary()methodhasEmployeeasthereceivertype */ func(eEmployee)displaySalary(){ fmt.Printf("Salaryof%sis%s%d",e.name,e.currency,e.salary) } funcmain(){ emp1:=Employee{ name:"SamAdolf", salary:5000, currency:"$", } emp1.displaySalary()//CallingdisplaySalary()methodofEmployeetype }123456789101112131415161718192021222324252627
上面程序的第6行,我们创建了Employee
的一个名为displaySalary
的方法。在displaySalary()
方法内部可以访问它的接收者e
(类型为Employee
)。在第17行,我们使用接收者e
,并打印它的name
,currency
以及salary
。
在第26行,我们使用emp1.displaySalary()
这样的语法来调用方法。
程序的输出为:SalaryofSamAdolfis$5000
。
为什么使用方法而不是函数?
上面的程序可以使用函数而不是方法重写如下:
packagemain import( "fmt" ) typeEmployeestruct{ namestring salaryint currencystring } /* displaySalary()methodconvertedtofunctionwithEmployeeasparameter */ funcdisplaySalary(eEmployee){ fmt.Printf("Salaryof%sis%s%d",e.name,e.currency,e.salary) } funcmain(){ emp1:=Employee{ name:"SamAdolf", salary:5000, currency:"$", } displaySalary(emp1) }123456789101112131415161718192021222324252627
在上面的程序中,我们使用displaySalary
函数替换了方法,并将Employee
结构体作为参数传给它。该程序的输出与上面的程序输出一样:SalaryofSamAdolfis$5000
。
那么为什么我们可以用函数完成同样的工作,却还要使用方法呢?这里有几个原因,我们一个一个地看。
- Go不是一个纯面向对象的编程语言,它不支持class类型。因此通过在一个类型上建立方法来实现与class相似的行为。
- 同名方法可以定义在不同的类型上,但是Go不允许同名函数。假设我们有一个
Square
和Circle
两个结构体。在Square
和Circle
上定义同名的方法是合法的,比如下面的程序:
packagemain import( "fmt" "math" ) typeRectanglestruct{ lengthint widthint } typeCirclestruct{ radiusfloat64 } func(rRectangle)Area()int{ returnr.length*r.width } func(cCircle)Area()float64{ returnmath.Pi*c.radius*c.radius } funcmain(){ r:=Rectangle{ length:10, width:5, } fmt.Printf("Areaofrectangle%d\n",r.Area()) c:=Circle{ radius:12, } fmt.Printf("Areaofcircle%f",c.Area()) }1234567891011121314151617181920212223242526272829303132333435
程序的输出为:
Areaofrectangle50
Areaofcircle452.38934212
接口正是应用了这一点(译者注:相同的方法名可以用在不同的接收者类型上)。我们将在下面的教程中讨论接口的细节。
指针接收者vs.值接收者
目前为止我们看到的都是以值作为接收者。以指针为接收者也是可以的。两者的区别在于,以指针作为接收者时,方法内部对其的修改对于调用者是可见的,但是以值作为接收者却不是。让我们通过下面的程序帮助理解。
packagemain import( "fmt" ) typeEmployeestruct{ namestring ageint } /* Methodwithvaluereceiver */ func(eEmployee)changeName(newNamestring){ e.name=newName } /* Methodwithpointerreceiver */ func(e*Employee)changeAge(newAgeint){ e.age=newAge } funcmain(){ e:=Employee{ name:"MarkAndrew", age:50, } fmt.Printf("Employeenamebeforechange:%s",e.name) e.changeName("MichaelAndrew") fmt.Printf("\nEmployeenameafterchange:%s",e.name) fmt.Printf("\n\nEmployeeagebeforechange:%d",e.age) (&e).changeAge(51) fmt.Printf("\nEmployeeageafterchange:%d",e.age) }1234567891011121314151617181920212223242526272829303132333435363738
上面的程序中,changeName
方法有一个值接收者(eEmployee)
,而changeAge
方法有一个指针接收者(e*Employee)
。在changeName
中改变Employee
的字段name
的值对调用者而言是不可见的,因此程序在调用e.changeName("MichaelAndrew")
方法之前和之后,打印的name
是一致的。而因为changeAge
的接受者是一个指针(e*Employee)
,因此通过调用方法(&e).changeAge(51)
来修改age
对于调用者是可见的。程序的输出如下:
Employeenamebeforechange:MarkAndrew
Employeenameafterchange:MarkAndrew
Employeeagebeforechange:50
Employeeageafterchange:5112345
在上面的程序第36行,我们用(&e).changeAge(51)
来调用changeAge
方法。因为changeAge
有一个指针类型的接收者我们必须使用(&e)
来调用。这不是必须的,Go允许我们省略&
符号,因此可以只写为e.changeAge(51)
。Go将e.changeAge(51)
解析为(&e).changeAge(51)
。
下面的程序使用e.changeAge(51)
来替代(&e).changeAge(51)
。它与上面的程序的打印结果是一样的。
packagemain import( "fmt" ) typeEmployeestruct{ namestring ageint } /* Methodwithvaluereceiver */ func(eEmployee)changeName(newNamestring){ e.name=newName } /* Methodwithpointerreceiver */ func(e*Employee)changeAge(newAgeint){ e.age=newAge } funcmain(){ e:=Employee{ name:"MarkAndrew", age:50, } fmt.Printf("Employeenamebeforechange:%s",e.name) e.changeName("MichaelAndrew") fmt.Printf("\nEmployeenameafterchange:%s",e.name) fmt.Printf("\n\nEmployeeagebeforechange:%d",e.age) e.changeAge(51) fmt.Printf("\nEmployeeageafterchange:%d",e.age) }1234567891011121314151617181920212223242526272829303132333435363738
何时使用指针接收者,何时使用值接收者?
一般来讲,指针接收者可用于对接收者的修改应该对调用者可以见的场合。
指针接收者也可用于拷贝结构体代价较大的场合。考虑一个包含较多字段的结构体,若使用值作为接收者则必须拷贝整个结构体,这样的代价很大。这种情况下使用指针接收者将避免结构体的拷贝,而仅仅是指向结构体指针的拷贝。
其他情况下可以使用值接收者。
匿名字段函数
匿名字段的方法可以被包含该匿名字段的结构体的变量调用,就好像该匿名字段的方法属于包含该字段的结构体一样。
packagemain import( "fmt" ) typeaddressstruct{ citystring statestring } func(aaddress)fullAddress(){ fmt.Printf("Fulladdress:%s,%s",a.city,a.state) } typepersonstruct{ firstNamestring lastNamestring address } funcmain(){ p:=person{ firstName:"Elon", lastName:"Musk", address:address{ city:"LosAngeles", state:"California", }, } p.fullAddress()//accessingfullAddressmethodofaddressstruct }12345678910111213141516171819202122232425262728293031323334
在上面的程序中,第32行,我们通过p.fullAddress()
调用了address
的方法fullAddress()
。像p.address.fullAddress()
这样的直接调用是不必要的。程序输出为:
Fulladdress:LosAngeles,California1
方法的值接收者vs.函数的值参数
这是很多新手遇到的问题。我们将尽可能把它说明白。
当一个函数有一个值参数时,它只接受一个值参数。
当一个方法有一个值接收者时,它可以接受值和指针接收者。
让我们通过程序说明这一点。
packagemain import( "fmt" ) typerectanglestruct{ lengthint widthint } funcarea(rrectangle){ fmt.Printf("AreaFunctionresult:%d\n",(r.length*r.width)) } func(rrectangle)area(){ fmt.Printf("AreaMethodresult:%d\n",(r.length*r.width)) } funcmain(){ r:=rectangle{ length:10, width:5, } area(r) r.area() p:=&r /* compilationerror,cannotusep(type*rectangle)astyperectangle inargumenttoarea */ //area(p) p.area()//callingvaluereceiverwithapointer }123456789101112131415161718192021222324252627282930313233343536
第12行,函数funcarea(rrectangle)
接受一个值参数,而方法func(rrectangle)area()
接受一个值接收者。
在第25行,我们传递了一个值来调用area
函数area(r)
,它将工作。同样地,我们通过值接收者调用area
方法r.area()
它也可以工作。
在第28行,我们创建了一个指向r
的指针p
。如果我们试图将这个指针传递给只接受值的area
函数那么编译器将报错:compilationerror,cannotusep(type*rectangle)astyperectangleinargumenttoarea.
。这是我们预期的。
现在来到了微妙的地方,第35行p.area()
使用指针接收者p
调用了接受一个值接收者的方法area
。这是完全合法的。原因是对于p.area()
,Go将其解析为(&p).area()
,因为area
方法必须接受一个值接收者。这是为了方便Go给我们提供的语法糖。
程序的输出为:
AreaFunctionresult:50
AreaMethodresult:50
AreaMethodresult:50123
方法的指针接收者vs.函数的指针参数
与值参数相似,一个接受指针参数的函数只能接受指针,而一个接收者为指针的方法可以接受值接收者和指针接收者。
packagemain
import(
"fmt"
)
typerectanglestruct{
lengthint
widthint
}
funcperimeter(r*rectangle){
fmt.Println("perimeterfunctionoutput:",2*(r.length+r.width))
}
func(r*rectangle)perimeter(){
fmt.Println("perimetermethodoutput:",2*(r.length+r.width))
}
funcmain(){
r:=rectangle{
length:10,
width:5,
}
p:=&r//pointertor
perimeter(p)
p.perimeter()
/*
cannotuser(typerectangle)astype*rectangleinargumenttoperimeter
*/
//perimeter(r)
r.perimeter()//callingpointerreceiverwithavalue
}123456789101112131415161718192021222324252627282930313233343536
在上面的程序中,第12行定义了一个函数perimeter
,该函数接受一个指针作为参数,而17行定义了一个方法,有一个指针接收者。
27行我们通过指针参数调用perimeter
函数,在第28行我们通过一个指针接收者调用perimeter
方法。一切都好。
在被注释掉的第33行,我们试图通以一个值r
调用perimeter
函数。这是非法的,因为一个接受指针为参数的函数不能接受一个值作为参数。如果去掉注释运行程序,则编译将报错:main.go:33:cannotuser(typerectangle)astype*rectangleinargumenttoperimeter.
。
在第35行我们通过一个值接收者r
调用接受一个指针接收者的perimeter
方法。这是合法的,r.perimeter()
这一行将被Go解析为(&r).perimeter()
。这是为了方便Go给我们提供的语法糖。程序的输出为:
perimeterfunctionoutput:30
perimetermethodoutput:30
perimetermethodoutput:30123
定义非结构体类型的方法
现在我们定义的都是结构体类型的方法。同样可以定义非结构体类型的方法,不过这里需要注意一点。为了定义某个类型的方法,接收者类型的定义与方法的定义必须在同一个包中。目前为止,我们定义的结构体和相应的方法都是在main包中的,因此没有任何问题。
packagemain func(aint)add(bint){ } funcmain(){ }12345678
在上面的程序中,第3行我们试图添加一个方法add
给内置类型int
。这是不允许的,因为定义方法add
所在的包和定义类型int
的包不是同一个包。这个程序将会报编译错误:cannotdefinenewmethodsonnon-localtypeint
。
使其工作的方法为定义内置类型的别名,然后以这个新类型为接收者创建方法。
packagemain import"fmt" typemyIntint func(amyInt)add(bmyInt)myInt{ returna+b } funcmain(){ num1:=myInt(5) num2:=myInt(10) sum:=num1.add(num2) fmt.Println("Sumis",sum) }12345678910111213141516
上面的程序中,第5行,我们创建了新的类型,一个int
的别名myInt
。在第7行,我们定义了一个方法add
,以myInt
作为接收者。
程序的输出为:Sumis15
。