Bài 15 - Một số đặc điểm hàm trong Go (tiếp theo)


  • Trùm cuối

    Trong bài này chúng ta sẽ tiếp tục tìm hiểu về các đặc điểm hàm trong Go gồm hàm vô danh, hàm bất định và lệnh trì hoãn defer.

    Hàm vô danh

    Trong ví dụ hàm cài đặt thuật toán Caesar ở bài trước chúng ta thấy tại dòng 7, hàm xử lý được cài đặt mà không có tên. Hàm như vậy sẽ được gọi là hàm vô danh.

    Hàm vô danh khai báo y như hàm chỉ có điều nó không có tên và được cài đặt ở trong một hàm khác. Một đặc điểm thú vị của hàm vô danh là nó có thể sử dụng các biến có phạm vi sử dụng trong hàm tạo nó mà không cần truyền tham số vào nó. Như ở ví dụ bài trước chúng ta thấy biến k có thể sử dụng trong hàm xử lý thuật toán Caesar dù nó có phạm vi ở hàm main.

    Chúng ta cùng khảo sát ví dụ khác:

    func squares() func() int { 
          var x int 
          return func() int { 
             x++ 
             return x * x 
          } 
    } 
    
    • Hàm squares trả về hàm vô danh mà hàm vô danh này trả về kiểu int.
    • Dòng 2 khai báo biến x cho hàm squares.
    • Dòng 3 trả về hàm vô danh được cài đặt ở dòng 3-6:
      💋 Dòng 3 khai báo hàm vô danh không có tham số, trả về giá trị kiểu int.
      💋 Dòng 4 tăng giá trị x (biến cục bộ của hàm squares) lên 1
      💋 Dòng 5 trả về bình phương của x.
    • Ở hàm main gán f := squares rồi in kết quả gọi liên tiếp f() 4 lần ta có kết quả là 1, 4, 9, 16. Lạ quá hen!? Chúng ta cùng phân tích nhé:
      💋 Ở lần gọi đầu tiên, biến x của squares được tạo và do không gán gì cả nên Go cho giá trị mặc định là 0. Hàm vô danh thực thi tăng biến x lên 1 tức lúc này x = 1. Sau đó hàm vô danh trả về tích của x tức là trả về 1. squares trả về hàm này mà hàm này trả về 1 nên ta có giá trị 1.
      💋 f lưu giá trị hàm squares nên lúc này giá trị x trong f đang là 1 nên khi gọi f() lần thứ 2 thì hàm vô danh nhận giá trị x là 1, nó tăng x lên 2 nên trả về tích là 4. Tương tự như vậy ở các lần gọi sau ta có giá trị là bình phương của 3 và 4.

    Ví dụ trên cho thấy rõ f tham chiếu đến hàm squares và vì những đặc điểm như vậy dẫn đến việc kiểu dữ liệu hàm được xếp thuộc nhóm kiểu dữ liệu tham chiếu như con trỏ, slice hay map.

    Cách khai báo và sử dụng hàm vô danh cũng như kiểu dữ liệu hàm trong Go là cách cài đặt sử dụng của kỹ thuật closures phổ biến ở một số ngôn ngữ lập trình.

    Trong trường hợp muốn dùng đệ quy với hàm vô danh, chúng ta cần khai báo biến kiểu hàm trước khi cài đặt hàm vô danh thay vì gán biến cho hàm vô danh và cài đặt luôn do lúc đó biến chưa được khai báo. Nên:

    var f func(int) int 
    f = func (n int) int { 
        ...
        f(x) 
        ...
    }
    

    thay vì:

    f := func (n int) int { 
        ... 
        f(x)   // lỗi biên dịch: undefined: f 
        ... 
    }
    

    Một lưu ý khác khi dùng hàm vô danh ở trong vòng lặp for: tất cả các biến được tạo bên ngoài for hay trên vòng for thì đều được hàm vô danh tham chiếu chứ không lấy giá trị. Hàm vô danh chỉ lấy giá trị các biến được khai báo trong vòng lặp for.

    Chúng ta cùng khảo sát ví dụ sau: tạo 1 dãy các thư mục con với tên lấy từ tham số dòng lệnh sau đó sẽ xóa các thư mục này đi khi nhấn phím bất kỳ:

    if len(os.Args) < 2 { 
        fmt.Println("Vui lòng nhập tên các thư mục cần tạo: dir thưmục1 ... thưmụcn") 
        return 
    } 
        
    var rmdirs []func() 
    for _, dir := range os.Args[1:] { 
        d := dir // Quan trọng! 
        os.MkdirAll(d, 0755) 
        rmdirs = append(rmdirs, func() { 
            os.RemoveAll(d) 
        }) 
    } 
    fmt.Print("Các thư mục đã được tạo. Nhấn phím bất kỳ để xóa chúng!") 
    var c string 
    fmt.Scanf("%s", &c) 
    for _, rmdir := range rmdirs { 
        rmdir() // Xóa thư mục 
    } 
    
    • Dòng 1-4 kiểm tra tham số dòng lệnh để đảm bảo có nhập ít nhất 1 tên thư mục.
    • Dòng 6 khai báo biến rmdirs là slice kiểu hàm không tham số không trả về.
    • Dòng 7-13 khai báo vòng lặp for duyệt các phần tử của slice được tạo từ slice os.Args bỏ phần tử đầu tiên (tên chương trình) và ta có slice mới chứa danh sách các tên thư mục cần tạo.
      💋 Dòng 8 tạo biến d gán giá trị biến dir cho d. Bước này thể hiện lưu ý bên trên. Chúng ta sẽ phân tích tại sao như vậy sau.
      💋 Dòng 9 tạo thư mục có tên chứa trong d nhờ hàm os.MkdirAll của package os.
      💋 Dòng 10 tạo 1 hàm vô danh rồi thêm vào slice rmdirs. Hàm vô danh này thực hiện xóa thư mục có tên là giá trị của d (dòng 11)
    • Dòng 14 thông báo việc tạo thư mục thành công và yêu cầu nhấn phím bất kỳ để thực hiện xóa thư mục. Mục đích việc này để ta có thể xem danh sách thư mục được tạo trước khi xóa.
    • Dòng 15-16 cài đặt lệnh chờ nhận nhập 1 phím.
    • Dòng 17-19 là vòng for thực hiện quét các phần tử của rmdirs và tiến hành thực thi hàm xóa thư mục tại từng phần tử.

    Kết quả như màn hình sau: gồm 3 phần

    • Trạng thái thư mục dir trước khi thực thi chương trình.
    • Trạng thái thư mục dir khi thực thi với tên các thư mục abcd, asdf, ghjk và qwerty từ tham số dòng lệnh.
    • Trạng thái thư mục dir sau khi nhấn phím bất kỳ.

    Bây giờ, giả dụ ta bỏ qua dòng 8, dùng trực tiếp biến dir ở dòng 9 và 11 thì tất cả các hàm vô danh đều tham chiếu đến biến dir. Khi ra khỏi vòng for, giá trị của dir lúc này là giá trị phần tử cuối cùng của slice chứa danh sách tên thư mục nên nếu thực hiện lệnh xóa như ở dòng 17-19 chúng ta chỉ xóa thư mục cuối cùng mà thôi. Các thư mục còn lại còn nguyên. Do vậy để đảm bảo giá trị duy nhất ở các hàm vô danh, ta phải khai báo biến cục bộ trong vòng for và sử dụng nó.

    Hàm bất định

    Hàm bất định là hàm có số lượng tham số không cố định khi khai báo. Ví dụ hay gặp nhất là hàm Print và các biến thể của nó trong gói fmt vốn chỉ cố định tham số đầu tiên còn số lượng các tham số còn lại thì tùy biến.

    Để khai báo một hàm bất định, chúng ta dùng dấu 3 chấm ...` để mô tả chuỗi tham số bất định.

    func sum(vals... int) int { 
        total := 0 
        for _, val := range vals { 
            total += val 
        } 
        return total 
    }
    
    • Dòng 1 khai báo hàm với tham số bất định vals kiểu int trả về giá trị int. Lưu ý dấu ... sát sau vals. Đây là hàm tính tổng các tham số nhập vào.
    • Dòng 2 tạo và gán 0 cho tổng total.
    • Dòng 3-5 là vòng lặp for quét các phần tử của vals rồi cộng dồn giá trị từng phần tử ở val vào total. Ở đây chúng ta thấy là vals được xử lý bên trong hàm như một slice kiểu int.
    • Dòng 6 trả tổng về.
    • Khi gọi sum(1,2,3,4,5) sẽ cho giá trị 15. Nếu chúng ta có sẵn slice a {1,2,3,4, 5} thì chúng ta có thể gọi hàm sum cho slice a như sau: sum(a...)

    Trường hợp muốn tạo tham số bất định gồm nhiều kiểu dữ liệu khác nhau thì chúng ta khai báo kiểu dữ liệu là interface{}. Chúng ta sẽ tìm hiểu kỹ về kiểu interface sau.

    Lệnh trì hoãn

    Trong quá trình lập trình, chúng ta hay phải sử dụng tài nguyên do hệ thống cấp phát. Sau khi sử dụng xong, chúng ta phải thông báo để hệ thống thu hồi hay giải phóng tài nguyên để các bên khác còn có thể sử dụng được tài nguyên đó. Với các hàm dài, xử lý phức tạp thì việc quên gọi hàm thông báo yêu cầu giải phóng tài nguyên là rất phổ biến nhất là khi có nhiều nơi có thể kết thúc hàm qua return.

    Go hiểu vấn đề đó và cung cấp defer cho chúng ta. Lệnh trì hoãn defer là lệnh khai báo trước, thực thi sau ngay trước khi kết thúc hàm. Với Go, sau khi xin cấp phát tài nguyên là chúng ta gọi ngay hàm thông báo thu hồi tài nguyên liền kèm từ khóa defer phía trước.

    Khi thực thi, Go sẽ không thực hiện thu hồi ngay khi nhận lệnh mà chờ đến khi kết thúc hàm mới thực thi. Cơ chế này khá giống một số lệnh ensure hay finally trong một số ngôn ngữ lập trình khác.

    package main 
    
    import ( 
        "fmt" 
        "os" 
    ) 
        
    func main() { 
        f := createFile("defer.txt") 
        defer closeFile(f) 
        writeFile(f) 
    } 
    
    func createFile(p string) *os.File { 
        fmt.Println("creating") 
        f, err := os.Create(p) 
        if err != nil { 
            panic(err) 
        } 
        return f 
    } 
    func writeFile(f *os.File) { 
        fmt.Println("writing") 
        fmt.Fprintln(f, "data") 
    } 
    func closeFile(f *os.File) { 
        fmt.Println("closing") 
        f.Close() 
    } 
    
    • Dòng 1-6 khai báo package và package cần sử dụng.
    • Dòng 14-21 khai báo hàm tạo file nhận tham số là chuỗi tên file và trả về con trỏ đến đối tượng os.File, là đối tượng quản lý các tác vụ liên quan file.
    func createFile(p string) *os.File { 
         fmt.Println("creating") 
         f, err := os.Create(p) 
          if err != nil { 
             panic(err) 
         } 
         return f 
    }
    

    💋 Dòng 16 sử dụng os.Create để tạo file, trả về con trỏ đối tượng os.File và biến lỗi.
    💋 Dòng 17-19 kiểm tra việc tạo file có lỗi hay không và ngưng chương trình, xuất lỗi nếu có lỗi thông qua hàm panic.
    💋 Dòng 20 trả về đối tượng file os.File.

    • Dòng 22-25 khai báo hàm ghi file nhận tham số là đối tượng file os.File. Sau đó tiến hành ghi nội dung "data" vào file vừa tạo thông qua con trỏ file. Việc ghi xuống file nhờ hàm fmt.Fprintln ở dòng 24.
    func writeFile(f *os.File) { 
         fmt.Println("writing") 
         fmt.Fprintln(f, "data") 
    }
    
    • Dòng 26-29 khai báo hàm đóng file để giải phóng tài nguyên nhận tham số đối tượng file và gọi hàm Close() tương ứng ở dòng 28.
    func closeFile(f *os.File) { 
        fmt.Println("closing") 
        f.Close() 
    }
    
    • Dòng 8-12 khai báo hàm main thực hiện lệnh tạo file, ghi file rồi đóng file. Mỗi lệnh đó thực thi hàm tương ứng kèm câu xuất ra màn hình ở dòng 15, 23 và 27 để chúng ta thấy thứ tự thực thi.
    func main() { 
         f := createFile("defer.txt") 
         defer closeFile(f) 
         writeFile(f) 
    }
    
    • Dòng 9 gọi hàm tạo file và gán giá trị trả về là đối tượng file cho biến f.
    • Dòng 10 gọi hàm đóng file f với từ khóa defer ở trước.
    • Dòng 11 gọi hàm ghi nội dung xuống file f.

    Điều gì sẽ xảy ra nếu chúng ta bỏ defer ở dòng 10? Kết quả sẽ xuất ra là creating, closing rồi mới writing. Và file defer.txt sẽ không có dữ liệu do file đã đóng trước khi ghi dữ liệu. Nếu chúng ta lấy thông tin lỗi ở hàm fmt.Fprintln ở dòng 24 thì sẽ thấy Go báo: "write defer.txt: The handle is invalid."

    Defer thường được dùng với các cặp lệnh OpenClose, ConnectDisconnect, hay LockUnlock. Chúng ta nên khai báo defer ngay sau khi gọi lệnh cấp tài nguyên nếu việc cấp tài nguyên thành công.

    Một số lưu ý khi sử dụng defer:

    • Giá trị tham số của hàm được gọi kèm với defer sẽ nhận giá trị tại thời điểm defer được gọi chứ không phải thời điểm hàm này thực thi.
    func a() { 
        i := 0 
        defer fmt.Println(i) 
        i++ 
        return 
    }
    

    Khi thực thi thì giá trị i được in ra là 0 do lúc gọi defer thì hàm Println nhận giá trị i = 0 nên dù thực hiện cuối hàm nó vẫn in giá trị 0.

    • Khi có nhiều defer trong một hàm thì các lệnh defer thực hiện theo cơ chế, gọi sau thực hiện trước. Trong ví dụ bên dưới thì kết quả in ra là "3210"
    func b() { 
        for i := 0; i < 4; i++ { 
            defer fmt.Print(i) 
        } 
    }
    
    • Do defer thực hiện sau cùng ở hàm, sau cả lệnh return nên nếu sử dụng defer hàm vô danh thì hàm vô danh có thể nhận giá trị biến trả về mới nhất.
    func double(x int) (result int) { 
        defer func() { fmt.Printf("double(%d) = %d\n", x, result) }() 
        return x + x 
    }
    

    Ở đây khi thực hiện double (3) ta sẽ có kết quả là 6 do hàm vô danh thực hiện sau cùng nên biến result tại thời điểm thực thi nó đã có giá trị là 6.

    • Nếu defer hàm vô danh và hàm này thay đổi giá trị biến trả về thì giá trị mà hàm trả về cũng sẽ thay đổi theo kết quả mà hàm vô danh mang lại.

    • Do hàm với defer thực hiện sau cùng ở hàm nên nếu cần mở, đóng hàng loạt tài nguyên trong vòng lặp for thì không nên defer hàm đóng tài nguyên trong vòng lặp for vì nó sẽ không đóng ngay dẫn đến việc mở nhiều tài nguyên và có thể khiến tài nguyên cạn kiệt. Ví dụ:

    for _, filename := range filenames { 
         f, err := os.Open(filename) 
         if err != nil { 
             return err 
        } 
        defer f.Close() // Nguy hiểm, có thể gây cạn tài nguyên 
        // ...xử lý f... 
    }
    

    Ở ví dụ này các file sẽ không được đóng cho đến khi thực hiện xong hàm chứa vòng lặp này. Cách giải quyết là tách cụm xử lý file từ lúc mở, defer đóng file cho đến xử lý vào chung 1 hàm và vòng for gọi hàm này. Như vậy mỗi lần lặp thì file cũ đã được mở và đóng xong trong hàm riêng xử lý file.

    Trong bài tới chúng ta sẽ tìm hiểu về xử lý lỗi trong Go.

    Tóm tắt

    • Hàm vô danh:
      💋 Hàm không đặt tên, khai báo các phần khác tương tự như hàm bình thường.
      💋 Hàm vô danh khai báo trong hàm khác và nó có thể sử dụng biến cục bộ của hàm chứa nó mà không cần truyền tham số.
      💋 Muốn dùng đệ quy với hàm vô danh thì biến gán cho hàm vô danh cần khai báo trước khi khai báo hàm vô danh.
      💋 Hàm vô danh trong vòng for nhận giá trị biến cục bộ trong vòng for còn các biến bên ngoài và trên vòng for hàm vô danh nhận tham chiếu.

    • Hàm bất định:
      💋 Là hàm có tham số không cố định.
      💋 Dùng dấu ... sau biến tham số để xác định tham số bất định

    • Lệnh trì hoãn:
      💋 Là lệnh khai báo với từ khóa defer giúp lệnh không thực thi ngay mà thực thi vào cuối hàm, sau cả lệnh return.
      💋 Defer thường dùng để báo đóng/hủy tài nguyên đã sử dụng.
      💋 Không nên dùng defer đóng tài nguyên trong vòng for vì sẽ tạo ra hàng loạt lệnh thực thi cuối hàm dẫn đến ngốn tài nguyên.



Có thể bạn cũng quan tâm

.
DMCA.com Protection Status