ฟังก์ชันใน Go
ใน Go ฟังก์ชันเป็นพลเมืองชั้นหนึ่ง ฟังก์ชันเป็นส่วนพื้นฐานที่สุดของ Go และเป็นหัวใจหลักของ Go
การประกาศ
รูปแบบการประกาศฟังก์ชันดังนี้
func ชื่อฟังก์ชัน([รายการพารามิเตอร์]) [ค่าส่งคืน] {
เนื้อหาฟังก์ชัน
}มีสองวิธีในการประกาศฟังก์ชัน วิธีแรกคือประกาศโดยตรงผ่านคีย์เวิร์ด func วิธีที่สองคือประกาศผ่านคีย์เวิร์ด var ดังนี้
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}ลายเซ็นฟังก์ชันประกอบด้วยชื่อฟังก์ชัน รายการพารามิเตอร์ และค่าส่งคืน ด้านล่างเป็นตัวอย่างที่สมบูรณ์ ชื่อฟังก์ชันคือ Sum มีพารามิเตอร์ประเภท int สองตัวคือ a, b ประเภทค่าส่งคืนคือ int
func Sum(a int, b int) int {
return a + b
}มีจุดที่สำคัญมากอย่างหนึ่งคือ ฟังก์ชันใน Go ไม่รองรับโอเวอร์โหลด เช่นโค้ดด้านล่างไม่สามารถผ่านการคอมไพล์ได้
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}ปรัชญาของ Go คือ หากลายเซ็นไม่เหมือนกันนั่นคือฟังก์ชันที่ต่างกันสองฟังก์ชันโดยสิ้นเชิง ดังนั้นไม่ควรใช้ชื่อเดียวกัน การโอเวอร์โหลดฟังก์ชันจะทำให้โค้ดสับสนและเข้าใจยาก ปรัชญานี้ถูกต้องหรือไม่ขึ้นอยู่กับมุมมอง อย่างน้อยใน Go คุณสามารถรู้ได้ว่ามันทำอะไรผ่านชื่อฟังก์ชันเท่านั้น โดยไม่ต้องหาว่ามันคือโอเวอร์โหลดไหน
พารามิเตอร์
พารามิเตอร์ใน Go สามารถไม่มีชื่อได้ โดยทั่วไปใช้เฉพาะเมื่อประกาศอินเทอร์เฟซหรือประเภทฟังก์ชัน แต่เพื่อความสามารถในการอ่านแนะนำให้ใส่ชื่อพารามิเตอร์ให้มากที่สุด
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}สำหรับพารามิเตอร์ที่มีประเภทเดียวกัน สามารถประกาศประเภทเพียงครั้งเดียว แต่เงื่อนไขคือต้องอยู่ติดกัน
func Log(format string, a1, a2 any) {
...
}พารามิเตอร์แบบแปรผันสามารถรับค่า 0 หรือมากกว่าได้ ต้องประกาศที่ท้ายสุดของรายการพารามิเตอร์ ตัวอย่างที่โดดเด่นที่สุดคือฟังก์ชัน fmt.Printf
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}ที่น่ากล่าวถึงคือ พารามิเตอร์ฟังก์ชันใน Go ส่งผ่านแบบค่า นั่นคือเมื่อส่งพารามิเตอร์จะคัดลอกค่าของอาร์กิวเมนต์ หากคุณคิดว่าการส่งสไลซ์หรือ map จะคัดลอกหน่วยความจำจำนวนมาก ฉันสามารถบอกคุณว่าไม่ต้องกังวล เพราะโครงสร้างข้อมูลทั้งสองนี้เป็นพอยน์เตอร์โดยพื้นฐาน
ค่าส่งคืน
ด้านล่างเป็นตัวอย่างค่าส่งคืนฟังก์ชันอย่างง่าย ฟังก์ชัน Sum ส่งคืนค่าประเภท int
func Sum(a, b int) int {
return a + b
}เมื่อฟังก์ชันไม่มีค่าส่งคืน ไม่ต้องการ void เพียงไม่ใส่ค่าส่งคืน
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go อนุญาตให้ฟังก์ชันมีค่าส่งคืนหลายค่า ในกรณีนี้ต้องใช้วงเล็บล้อมค่าส่งคืน
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 ไม่สามารถเป็นตัวถูกหารได้")
}
return a / b, nil
}Go ยังรองรับค่าส่งคืนที่มีชื่อ ต้องไม่ซ้ำกับชื่อพารามิเตอร์ เมื่อใช้ค่าส่งคืนที่มีชื่อ คีย์เวิร์ด return ไม่จำเป็นต้องระบุว่าส่งคืนค่าใด
func Sum(a, b int) (ans int) {
ans = a + b
return
}เช่นเดียวกับพารามิเตอร์ เมื่อมีค่าส่งคืนที่มีชื่อหลายตัวที่มีประเภทเดียวกัน สามารถละเว้นการประกาศประเภทที่ซ้ำกันได้
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}ไม่ว่าค่าส่งคืนที่มีชื่อจะประกาศอย่างไร ลำดับความสำคัญสูงสุดคือค่าหลังคีย์เวิร์ด return เสมอ
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c, d จะไม่ถูกส่งคืน
return a + b, a * b
}ฟังก์ชันไม่ระบุชื่อ
ฟังก์ชันไม่ระบุชื่อคือฟังก์ชันที่ไม่มีลายเซ็น เช่นฟังก์ชัน func(a, b int) int ด้านล่าง มันไม่มีชื่อ ดังนั้นเราต้องตามด้วยวงเล็บหลังเนื้อหาฟังก์ชันเพื่อเรียกใช้
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}เมื่อเรียกใช้ฟังก์ชันหนึ่ง เมื่อพารามิเตอร์ของมันเป็นประเภทฟังก์ชัน ในกรณีนี้ชื่อไม่สำคัญอีกต่อไป สามารถส่งฟังก์ชันไม่ระบุชื่อได้โดยตรง ดังนี้
type Person struct {
Name string
Age int
Salary float64
}
func main() {
people := []Person{
{Name: "Alice", Age: 25, Salary: 5000.0},
{Name: "Bob", Age: 30, Salary: 6000.0},
{Name: "Charlie", Age: 28, Salary: 5500.0},
}
slices.SortFunc(people, func(p1 Person, p2 Person) int {
if p1.Name > p2.Name {
return 1
} else if p1.Name < p2.Name {
return -1
}
return 0
})
}นี่เป็นตัวอย่างการกำหนดกฎการเรียงลำดับแบบกำหนดเอง slices.SortFunc รับพารามิเตอร์สองตัว หนึ่งคือสไลซ์ อีกตัวคือฟังก์ชันเปรียบเทียบ หากไม่พิจารณาการใช้ซ้ำ เราสามารถส่งฟังก์ชันไม่ระบุชื่อได้โดยตรง
คลอ저
คลอ저 (Closure) เป็นแนวคิดนี้ ในบางภาษาเรียกว่านิพจน์ Lambda ใช้ร่วมกับฟังก์ชันไม่ระบุชื่อ คลอ저 = ฟังก์ชัน + การอ้างอิงสภาพแวดล้อม ดูตัวอย่างด้านล่าง
func main() {
grow := Exp(2)
for i := range 10 {
fmt.Printf("2^%d=%d\n", i, grow())
}
}
func Exp(n int) func() int {
e := 1
return func() int {
temp := e
e *= n
return temp
}
}เอาต์พุต
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512ค่าส่งคืนของฟังก์ชัน Exp เป็นฟังก์ชันหนึ่ง ที่นี่จะเรียกว่าฟังก์ชัน grow ทุกครั้งที่เรียกใช้ ตัวแปร e จะเพิ่มขึ้นแบบเอกซ์โพเนนเชียลหนึ่งครั้ง ฟังก์ชัน grow อ้างอิงตัวแปรสองตัวของฟังก์ชัน Exp คือ e และ n พวกมันเกิดในขอบเขตของฟังก์ชัน Exp ในสถานการณ์ปกติ เมื่อการเรียกฟังก์ชัน Exp สิ้นสุดลง หน่วยความจำของตัวแปรเหล่านี้จะถูกกู้คืนเนื่องจากการออกจากสแต็ก แต่เนื่องจากฟังก์ชัน grow อ้างอิงพวกมัน พวกมันจึงไม่สามารถถูกกู้คืนได้ แต่หนีออกไปยังฮีป แม้ว่าการดำรงอยู่ของฟังก์ชัน Exp จะสิ้นสุดลงแล้ว แต่การดำรงอยู่ของตัวแปร e และ n ยังไม่สิ้นสุด ในฟังก์ชัน grow ยังสามารถแก้ไขตัวแปรทั้งสองนี้ได้โดยตรง ฟังก์ชัน grow คือฟังก์ชันคลอ저
การใช้คลอ저 สามารถสร้างฟังก์ชันสำหรับหาลำดับฟีโบนัชชีได้อย่างง่าย代码如下
func main() {
// จำนวนฟีโบนัชชี 10 ตัว
fib := Fib(10)
for n, next := fib(); next; n, next = fib() {
fmt.Println(n)
}
}
func Fib(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}เอาต์พุตคือ
0
1
1
2
3
5
8
13
21
34การเรียกแบบล่าช้า
คีย์เวิร์ด defer สามารถทำให้ฟังก์ชันเรียกหลังจากล่าช้าช่วงเวลาหนึ่ง ก่อนที่ฟังก์ชันจะส่งคืน ฟังก์ชันที่อธิบายโดย defer เหล่านี้จะถูกดำเนินการทีละตัวในที่สุด ดูตัวอย่างด้านล่าง
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}เอาต์พุต
2
1เนื่องจาก defer ดำเนินการก่อนที่ฟังก์ชันจะส่งคืน คุณยังสามารถแก้ไขค่าส่งคืนของฟังก์ชันใน defer ได้
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}เมื่อมีฟังก์ชันที่อธิบายโดย defer หลายฟังก์ชัน จะดำเนินการตามลำดับแบบสแต็ก คือเข้าก่อนออกหลัง
func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}0
2
5
4
3
1การเรียกแบบล่าช้ามักใช้สำหรับการปล่อยทรัพยากรไฟล์ ปิดการเชื่อมต่อเครือข่าย และการใช้งานอื่นๆ อีกประการหนึ่งคือการจับ panic แต่สิ่งนี้จะกล่าวถึงในบทการจัดการข้อผิดพลาด
ลูป
แม้จะไม่ห้ามอย่างชัดเจน แต่แนะนำให้ไม่ใช้ defer ในลูป for ดังนี้
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}เอาต์พุตดังนี้
4
3
2
1
0ผลลัพธ์ของโค้ดนี้ถูกต้อง แต่กระบวนการอาจไม่ถูกต้อง ใน Go ทุกครั้งที่สร้าง defer ต้องขอพื้นที่หน่วยความจำในโกoutine ปัจจุบัน สมมติว่าในตัวอย่างข้างต้นไม่ใช่ลูป for n ง่ายๆ แต่เป็นขั้นตอนวิธีประมวลผลข้อมูลที่ซับซ้อนกว่า เมื่อจำนวนคำขอจากภายนอกเพิ่มขึ้นอย่างกะทันหัน ในเวลาสั้นๆ จะสร้าง defer จำนวนมาก เมื่อจำนวนรอบลูปมากหรือไม่แน่นอน อาจทำให้การใช้หน่วยความจำเพิ่มขึ้นอย่างกะทันหัน สิ่งนี้โดยทั่วไปเราเรียกว่าการรั่วไหลของหน่วยความจำ
การคำนวณพารามิเตอร์ล่วงหน้า
สำหรับการเรียกแบบล่าช้ามีรายละเอียดที่ขัดกับสัญชาตญาณบางประการ เช่นตัวอย่างด้านล่าง
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}กับดักนี้ยังคงซ่อนเร้นมาก ผู้เขียนเคยติดกับดักนี้มาครึ่งวันไม่สามารถหาสาเหตุได้ ลองทายดูว่าเอาต์พุตคืออะไร คำตอบดังนี้
2
3
1หลายคนอาจคิดว่าเอาต์พุตเป็นแบบนี้
3
2
1ตามความตั้งใจของผู้ใช้ fmt.Println(Fn1()) ส่วนนี้ควรหวังให้ดำเนินการหลังจากที่เนื้อหาฟังก์ชันดำเนินการเสร็จสิ้น fmt.Println จริงๆ แล้วดำเนินการสุดท้าย แต่ Fn1() อยู่เหนือความคาดหมาย ตัวอย่างด้านล่างทำให้สถานการณ์ชัดเจนยิ่งขึ้น
func main() {
var a, b int
a = 1
b = 2
defer fmt.Println(sum(a, b))
a = 3
b = 4
}
func sum(a, b int) int {
return a + b
}เอาต์พุตของมันต้องเป็น 3 ไม่ใช่ 7 หากใช้คลอเซอร์แทนการเรียกแบบล่าช้า ผลลัพธ์จะต่างกัน
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}เอาต์พุตของคลอเซอร์คือ 7 แล้วถ้ารวมการเรียกแบบล่าช้าและคลอเซอร์เข้าด้วยกันล่ะ
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}ครั้งนี้ปกติ เอาต์พุตคือ 7 ต่อไปแก้ไขอีกครั้ง ไม่มีคลอเซอร์แล้ว
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}เอาต์พุตกลับมาเป็น 3 อีกแล้ว จากการเปรียบเทียบตัวอย่างข้างต้นจะพบว่าโค้ด这段
defer fmt.Println(sum(a,b))จริงๆ แล้วเทียบเท่ากับ
defer fmt.Println(3)Go ไม่รอจนสุดท้ายเพื่อเรียกฟังก์ชัน sum ฟังก์ชัน sum ถูกเรียกก่อนที่การเรียกแบบล่าช้าจะถูกดำเนินการ และส่งผ่านเป็นพารามิเตอร์ให้ fmt.Println สรุปคือ สำหรับฟังก์ชันที่ defer กระทำโดยตรง พารามิเตอร์ของมันจะถูกคำนวณล่วงหน้า ซึ่งนำไปสู่ปรากฏการณ์ประหลาดในตัวอย่างแรก สำหรับสถานการณ์นี้ โดยเฉพาะกรณีที่ส่งคืนค่าฟังก์ชันเป็นพารามิเตอร์ในการเรียกแบบล่าช้า ต้องระวังเป็นพิเศษ
