Skip to content

ฟังก์ชันใน Go

ใน Go ฟังก์ชันเป็นพลเมืองชั้นหนึ่ง ฟังก์ชันเป็นส่วนพื้นฐานที่สุดของ Go และเป็นหัวใจหลักของ Go

การประกาศ

รูปแบบการประกาศฟังก์ชันดังนี้

go
func ชื่อฟังก์ชัน([รายการพารามิเตอร์]) [ค่าส่งคืน] {
  เนื้อหาฟังก์ชัน
}

มีสองวิธีในการประกาศฟังก์ชัน วิธีแรกคือประกาศโดยตรงผ่านคีย์เวิร์ด func วิธีที่สองคือประกาศผ่านคีย์เวิร์ด var ดังนี้

go
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

go
func Sum(a int, b int) int {
   return a + b
}

มีจุดที่สำคัญมากอย่างหนึ่งคือ ฟังก์ชันใน Go ไม่รองรับโอเวอร์โหลด เช่นโค้ดด้านล่างไม่สามารถผ่านการคอมไพล์ได้

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 สามารถไม่มีชื่อได้ โดยทั่วไปใช้เฉพาะเมื่อประกาศอินเทอร์เฟซหรือประเภทฟังก์ชัน แต่เพื่อความสามารถในการอ่านแนะนำให้ใส่ชื่อพารามิเตอร์ให้มากที่สุด

go
type ExWriter func(io.Writer) error

type Writer interface {
  ExWrite([]byte) (int, error)
}

สำหรับพารามิเตอร์ที่มีประเภทเดียวกัน สามารถประกาศประเภทเพียงครั้งเดียว แต่เงื่อนไขคือต้องอยู่ติดกัน

go
func Log(format string, a1, a2 any) {
  ...
}

พารามิเตอร์แบบแปรผันสามารถรับค่า 0 หรือมากกว่าได้ ต้องประกาศที่ท้ายสุดของรายการพารามิเตอร์ ตัวอย่างที่โดดเด่นที่สุดคือฟังก์ชัน fmt.Printf

go
func Printf(format string, a ...any) (n int, err error) {
  return Fprintf(os.Stdout, format, a...)
}

ที่น่ากล่าวถึงคือ พารามิเตอร์ฟังก์ชันใน Go ส่งผ่านแบบค่า นั่นคือเมื่อส่งพารามิเตอร์จะคัดลอกค่าของอาร์กิวเมนต์ หากคุณคิดว่าการส่งสไลซ์หรือ map จะคัดลอกหน่วยความจำจำนวนมาก ฉันสามารถบอกคุณว่าไม่ต้องกังวล เพราะโครงสร้างข้อมูลทั้งสองนี้เป็นพอยน์เตอร์โดยพื้นฐาน

ค่าส่งคืน

ด้านล่างเป็นตัวอย่างค่าส่งคืนฟังก์ชันอย่างง่าย ฟังก์ชัน Sum ส่งคืนค่าประเภท int

go
func Sum(a, b int) int {
   return a + b
}

เมื่อฟังก์ชันไม่มีค่าส่งคืน ไม่ต้องการ void เพียงไม่ใส่ค่าส่งคืน

go
func ErrPrintf(format string, a ...any) {
  _, _ = fmt.Fprintf(os.Stderr, format, a...)
}

Go อนุญาตให้ฟังก์ชันมีค่าส่งคืนหลายค่า ในกรณีนี้ต้องใช้วงเล็บล้อมค่าส่งคืน

go
func Div(a, b float64) (float64, error) {
  if a == 0 {
    return math.NaN(), errors.New("0 ไม่สามารถเป็นตัวถูกหารได้")
  }
  return a / b, nil
}

Go ยังรองรับค่าส่งคืนที่มีชื่อ ต้องไม่ซ้ำกับชื่อพารามิเตอร์ เมื่อใช้ค่าส่งคืนที่มีชื่อ คีย์เวิร์ด return ไม่จำเป็นต้องระบุว่าส่งคืนค่าใด

go
func Sum(a, b int) (ans int) {
  ans = a + b
  return
}

เช่นเดียวกับพารามิเตอร์ เมื่อมีค่าส่งคืนที่มีชื่อหลายตัวที่มีประเภทเดียวกัน สามารถละเว้นการประกาศประเภทที่ซ้ำกันได้

go
func SumAndMul(a, b int) (c, d int) {
  c = a + b
  d = a * b
  return
}

ไม่ว่าค่าส่งคืนที่มีชื่อจะประกาศอย่างไร ลำดับความสำคัญสูงสุดคือค่าหลังคีย์เวิร์ด return เสมอ

go
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 ด้านล่าง มันไม่มีชื่อ ดังนั้นเราต้องตามด้วยวงเล็บหลังเนื้อหาฟังก์ชันเพื่อเรียกใช้

go
func main() {
   func(a, b int) int {
      return a + b
   }(1, 2)
}

เมื่อเรียกใช้ฟังก์ชันหนึ่ง เมื่อพารามิเตอร์ของมันเป็นประเภทฟังก์ชัน ในกรณีนี้ชื่อไม่สำคัญอีกต่อไป สามารถส่งฟังก์ชันไม่ระบุชื่อได้โดยตรง ดังนี้

go
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 ใช้ร่วมกับฟังก์ชันไม่ระบุชื่อ คลอ저 = ฟังก์ชัน + การอ้างอิงสภาพแวดล้อม ดูตัวอย่างด้านล่าง

go
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 คือฟังก์ชันคลอ저

การใช้คลอ저 สามารถสร้างฟังก์ชันสำหรับหาลำดับฟีโบนัชชีได้อย่างง่าย代码如下

go
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 เหล่านี้จะถูกดำเนินการทีละตัวในที่สุด ดูตัวอย่างด้านล่าง

go
func main() {
  Do()
}

func Do() {
  defer func() {
    fmt.Println("1")
  }()
  fmt.Println("2")
}

เอาต์พุต

2
1

เนื่องจาก defer ดำเนินการก่อนที่ฟังก์ชันจะส่งคืน คุณยังสามารถแก้ไขค่าส่งคืนของฟังก์ชันใน defer ได้

go
func main() {
  fmt.Println(sum(3, 5))
}

func sum(a, b int) (s int) {
  defer func() {
    s -= 10
  }()
  s = a + b
  return
}

เมื่อมีฟังก์ชันที่อธิบายโดย defer หลายฟังก์ชัน จะดำเนินการตามลำดับแบบสแต็ก คือเข้าก่อนออกหลัง

go
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 ดังนี้

go
func main() {
  n := 5
  for i := range n {
    defer fmt.Println(i)
  }
}

เอาต์พุตดังนี้

4
3
2
1
0

ผลลัพธ์ของโค้ดนี้ถูกต้อง แต่กระบวนการอาจไม่ถูกต้อง ใน Go ทุกครั้งที่สร้าง defer ต้องขอพื้นที่หน่วยความจำในโกoutine ปัจจุบัน สมมติว่าในตัวอย่างข้างต้นไม่ใช่ลูป for n ง่ายๆ แต่เป็นขั้นตอนวิธีประมวลผลข้อมูลที่ซับซ้อนกว่า เมื่อจำนวนคำขอจากภายนอกเพิ่มขึ้นอย่างกะทันหัน ในเวลาสั้นๆ จะสร้าง defer จำนวนมาก เมื่อจำนวนรอบลูปมากหรือไม่แน่นอน อาจทำให้การใช้หน่วยความจำเพิ่มขึ้นอย่างกะทันหัน สิ่งนี้โดยทั่วไปเราเรียกว่าการรั่วไหลของหน่วยความจำ

การคำนวณพารามิเตอร์ล่วงหน้า

สำหรับการเรียกแบบล่าช้ามีรายละเอียดที่ขัดกับสัญชาตญาณบางประการ เช่นตัวอย่างด้านล่าง

go
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() อยู่เหนือความคาดหมาย ตัวอย่างด้านล่างทำให้สถานการณ์ชัดเจนยิ่งขึ้น

go
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 หากใช้คลอเซอร์แทนการเรียกแบบล่าช้า ผลลัพธ์จะต่างกัน

go
func main() {
  var a, b int
  a = 1
  b = 2
  f := func() {
    fmt.Println(sum(a, b))
  }
  a = 3
  b = 4
  f()
}

เอาต์พุตของคลอเซอร์คือ 7 แล้วถ้ารวมการเรียกแบบล่าช้าและคลอเซอร์เข้าด้วยกันล่ะ

go
func main() {
  var a, b int
  a = 1
  b = 2
  defer func() {
    fmt.Println(sum(a, b))
  }()
  a = 3
  b = 4
}

ครั้งนี้ปกติ เอาต์พุตคือ 7 ต่อไปแก้ไขอีกครั้ง ไม่มีคลอเซอร์แล้ว

go
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 กระทำโดยตรง พารามิเตอร์ของมันจะถูกคำนวณล่วงหน้า ซึ่งนำไปสู่ปรากฏการณ์ประหลาดในตัวอย่างแรก สำหรับสถานการณ์นี้ โดยเฉพาะกรณีที่ส่งคืนค่าฟังก์ชันเป็นพารามิเตอร์ในการเรียกแบบล่าช้า ต้องระวังเป็นพิเศษ

Golang by www.golangdev.cn edit