Đây là bài hướng dẫn thứ bảy trong series hướng dẫn lập trình Golang cơ bản.

Packages là gì? Tại sao chúng ta phải dùng packages?

Trong các bài hướng dẫn trước, chúng ta thấy rằng một chương trình chạy bằng một file duy nhất có một hàm chính (main function) và một vài hàm khác. Trong các dự án thực tế, chúng ta không viết tất cả mã nguồn vào một file đơn lẻ như vậy. Chúng ta không thể tái sử dụng code hay bảo trì code theo cách viết này được. Để giải quyết vấn đề đó chúng ta phải cần đến packages (gói).

Packages được sử dụng để tổ chức mã nguồn sao cho việc đọc và tái sử dụng mã nguồn dễ dàng hơn. Packages giúp phân chia mã nguồn thành nhiều phần, do đó việc bảo trì ứng dụng cũng thuận tiện hơn.

Ví dụ, chúng ta cần tạo một ứng dụng xử lý hình ảnh với các tính năng như image cropping (cắt ảnh), sharpening (làm sắc nét), blurring (làm mờ) và color enhancement (tinh chỉnh màu sắc). Cách để xây dựng ứng dụng này là nhóm tất cả các code liên quan đến một tính năng vào trong một package riêng của nó. Ví dụ tính năng image cropping ở trong một package riêng, tính năng sharpening thì ở một package khác. Ưu điểm của việc này là, tính năng color enhancement có thể cần sử dụng một số hàm xử lý của tính năng sharpening. Khi đó ở mã nguồn của tính năng color enhancement ta có thể import (thêm vào) package sharpening và sử dụng các hàm của nó (chúng ta sẽ bàn về import sau). Bằng cách này code của chúng ta sẽ dễ dàng tái sử dụng.

Chúng ta sẽ thử tạo một ứng dụng tính toán diện tích và đường chéo của hình chữ nhật. Ta sẽ hiểu về packages rõ hơn thông qua ứng dụng này.

 

main function and main package

Một ứng dụng muốn thực thi cần có một main function (hàm chính). Hàm này là điểm bắt đầu (entry point) để thực thi. Main function phải nằm trong main package (gói chính).

Để chỉ định rằng một file mã nguồn cụ thể thuộc về một gói nào đó ta dùng câu lệnh sau package packagename. Và đây chính là dòng lệnh đầu tiên của mọi file mã nguồn.

Đầu tiên, chúng ta tạo main function và main package cho ứng dụng. Tạo một thư mục bên trong thư mục src của go workspace (không gian làm việc của go) và đặt tên cho nó, ví dụ geometry (hình học). Tạo một file tên là geometry.go bên trong thư mục geometry.

Viết đoạn code sau vào trong file geometry.go

//geometry.go
package main 

import "fmt"

func main() {  
    fmt.Println("Geometrical shape properties")
}

Câu lệnh package main cho biết file này thuộc về main package (gói chính). Câu lệnh import “packagename” dùng để import package đã có sẵn. Trong trường hợp này chúng ta import package fmt, package này có chứa phương thức Println. Sau đó ta có hàm main dùng để in ra dòng chữ Geometrical shape properties

Ta biên dịch chương trình trên bằng cách gõ go install geometry vào cửa sổ dòng lệnh. Lệnh này sẽ tìm kiếm một file có main function nằm trong thư mục geometry. Ở đây nó tìm thấy file geometry.go. File này sẽ được biên dịch và tạo ra một file nhị phân có tên geometry (ở hệ điều hành windows sẽ là file geometry.exe) nằm trong thư mục bin của workspace. Ta có cấu trúc của workspace như sau:

src  
    geometry
            gemometry.go
bin  
    geometry

Chạy chương trình bằng cách gõ workspacepath/bin/geometry, ở đây workspacepath chính là đường dẫn đến go workspace trên máy tính của bạn. Lệnh này để chạy file nhị phân geometry trong thư mục bin. Ta có output Geometrical shape properties như trên.

 

Tạo các package tùy chỉnh

Tiếp theo, chúng ta sẽ cấu trúc code theo hướng tất cả các chức năng liên quan đến hình chữ nhật (rectangle) sẽ nằm trong một package tên là rectangle

Tạo một package tùy chỉnh tên là rectangle có các chức năng như tính diện tích và đường chéo của một hình chữ nhật.

Các file mã nguồn của một package phải được đặt trong thư mục riêng của chúng. Quy ước đặt tên trong Go là thư mục phải trùng tên với tên của package.

Giờ chúng ta tạo một thư mục đặt tên là rectangle bên trong thư mục geometry. Tất cả các file mã nguồn nằm trong thư mục rectangle đều phải khai báo package rectangle ở dòng đầu tiên vì chúng đều thuộc về package rectangle.

Tạo file rectprops.go bên trong thư mục rectangle mà chúng ta vừa tạo, sau đó thêm vào đoạn code sau:

//rectprops.go
package rectangle

import "math"

func Area(len, wid float64) float64 {  
    area := len * wid
    return area
}

func Diagonal(len, wid float64) float64 {  
    diagonal := math.Sqrt((len * len) + (wid * wid))
    return diagonal
}

Ở đoạn code trên, chúng ta có 2 fucntion là Area (tính diện tích) và Diagonal (tính đường chéo). Diện tích hình chữ nhật là tích của chiều rộng và chiều dài. Đường chéo hình chữ nhật bằng căn bậc hai của tổng bình phương chiều rộng và bình phương chiều dài. Hàm Sprt thuộc package math dùng để tính căn bậc hai.

Lưu ý rằng tên hàm là AreaDiagonal viết hoa chữ cái đầu tiên. Điều này rất quan trọng và tôi sẽ giải thích ngay bên dưới.

 

Import các package tùy chỉnh

Để sử dụng các package tùy chỉnh ta phải import chúng. Cú pháp: import path. Chúng ta phải chỉ định đường dẫn đến package tùy chỉnh đối với thư mục src bên trong workspace. Cấu trúc thư mục hiện tại của chúng ta là:

src  
   geometry
           geometry.go
           rectangle
                    rectprops.go

Câu lệnh import "geometry/rectangle" sẽ import package rectangle.

Viết code như dưới đây vào file geometry.go

//geometry.go
package main 

import (  
    "fmt"
    "geometry/rectangle" //importing package tùy chỉnh
)

func main() {  
    var rectLen, rectWidth float64 = 6, 7
    fmt.Println("Geometrical shape properties")

        /*Hàm Area của package rectangle được sử dụng
        */
    fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
        /*Hàm Diagonal của package rectangle được sử dụng
        */
    fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}

Đoạn code trên import package rectangle và sử dụng 2 hàm AreaDiagonal của nó để tính toán diện tích và đường chéo hình chữ nhật. Định dạng %.2f trong hàm Printf dùng để in ra 2 chữ số thập phân phía sau dấu phẩy động. Ta có output của chương trình:

Geometrical shape properties  
area of rectangle 42.00  
diagonal of the rectangle 9.22

 

Exported names (tên dẫn xuất)

Hai hàm AreaDiagonal trong package rectangle được viết hoa chữ cái đầu. Điều này có ý nghĩa đặc biệt trong Go. Bất kỳ biến hoặc hàm nào bắt đầu bằng chữ cái viết hoa đều là exported names trong Go. Và chỉ những hàm hoặc biến là exported names thì mới có thể được truy xuất từ bên ngoài package. Trong trường hợp này, chúng ta cần truy cập hàm AreaDiagonal từ main package, do đó chúng phải được viết hoa chữ cái đầu.

Nếu ta thay đổi tên hàm từ Area(len, wid float64) thành area(len, wid float64) trong file rectprops.gorectangle.Area(rectLen, rectWidth) thành rectangle.area(rectLen, rectWidth) trong file geometry.go sau đó chạy chương trình, thì trình biên dịch sẽ ném ra lỗi geometry.go:11: cannot refer to unexported name rectangle.area. Do đó nếu chúng ta muốn truy cập đến một hàm ở bên ngoài package, ta phải viết hoa chữ cái đầu tiên.

 

init function (hàm khởi tạo)

Mỗi package có thể chứa một hàm được gọi là hàm init (hàm khởi tạo). Hàm init này không có tham số và không có kiểu trả về. Hàm init không được gọi một cách tường minh trong mã nguồn của chúng ta. Hàm init được khai báo như dưới đây:

func init() {  
}

Hàm init có thể được sử dụng để thực hiện các nhiệm vụ khởi tạo hoặc cũng có thể được sử dụng để xác minh tính chính xác của chương trình trước khi bắt đầu thực hiện.

Chúng ta cùng theo dõi thứ tự khởi tạo một package như sau:

  1. Các biến ở cấp độ package (biến toàn cục) được khởi tạo trước tiên.
  2. Hàm init được gọi kế tiếp. Một package có thể có nhiều hàm init (hoặc trong một file đơn lẻ hoặc được phân phối trên nhiều file) và chúng được gọi theo thứ tự biên dịch.

Nếu một package import một package khác, các package được import sẽ được khởi tạo trước tiên.

Một package được khởi tạo một lần duy nhất ngay cả khi nó được import từ nhiều package khác.

Chúng ta sẽ sửa đổi một chút đối với ứng dụng trên để hiểu hơn về hàm init.

Đầu tiên ta thêm hàm init vào file rectprops.go

//rectprops.go
package rectangle

import "math"  
import "fmt"

/*
 * Hàm init được thêm vào
 */
func init() {  
    fmt.Println("rectangle package initialized")
}
func Area(len, wid float64) float64 {  
    area := len * wid
    return area
}

func Diagonal(len, wid float64) float64 {  
    diagonal := math.Sqrt((len * len) + (wid * wid))
    return diagonal
}

Ta vừa thêm một hàm init đơn giản với chức năng in ra dòng chữ rectangle package initialized.

Giờ ta sẽ thay đổi ở main package. Ta biết rằng chiều dài và chiều rộng của hình chữ nhật phải lớn hơn 0. Chúng ta sẽ kiểm tra điều kiện này bằng cách sử dụng hàm init và các biến toàn cục trong file geometry.go

Thay đổi file geometry.go như dưới đây:

//geometry.go
package main 

import (  
    "fmt"
    "geometry/rectangle" //importing package tùy chỉnh
    "log"
)
/*
 * 1. Biến toàn cục (package variables)
*/
var rectLen, rectWidth float64 = 6, 7 

/*
*2. Hàm init để kiểm tra nếu chiều rộng và chiều dài nhỏ hơn 0
*/
func init() {  
    println("main package initialized")
    if rectLen < 0 {
        log.Fatal("length is less than zero")
    }
    if rectWidth < 0 {
        log.Fatal("width is less than zero")
    }
}

func main() {  
    fmt.Println("Geometrical shape properties")
    fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
    fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}

Ta đã thay đổi file geometry.go như sau:

  1. Hai biến rectLenrectWidth từ biến cục bộ thuộc hàm main đã được chuyển thành biến toàn cục.
  2. Hàm init đã được thêm vào. Hàm này in ra một bản ghi (log) và dừng chương trình nếu rectLen hoặc rectWidth < 0 bằng cách sử dụng hàm log.Fatal

 Thứ tự khởi tạo main package là:

  1. Các package được import sẽ khởi tạo trước. Do đó package rectangle được khởi tạo đầu tiên.
  2. Các biến toàn cục rectLenrectWidth được khởi tạo tiếp theo.
  3. Hàm init được gọi.
  4. Hàm main được gọi cuối cùng.

 Chạy chương trình, ta có output sau:

rectangle package initialized  
main package initialized  
Geometrical shape properties  
area of rectangle 42.00  
diagonal of the rectangle 9.22

Như ta thấy, hàm init của package rectangle được gọi đầu tiên, sau đó là khởi tạo các biến toàn cục rectLenrectWidth. Hàm init của package main được gọi tiếp theo. Nó sẽ kiểm tra xem rectLen và rectWidth có < 0 hay ko và kết thúc chương trình nếu điều kiện là đúng. Chúng ta sẽ tìm hiểu về câu lệnh if chi tiết hơn trong bài hướng dẫn riêng. Bây giờ bạn có thể giả định rằng câu lệnh if rectLen < 0 sẽ kiểm tra xem rectLen có nhỏ hơn 0 và nếu đúng thì dừng chương trình. Ta cũng viết một câu điều kiện tương tự để kiểm tra rectWidth. Trong trường hợp này cả hai điều kiện là sai và chương trình vẫn tiếp tục. Cuối cùng, hàm main được gọi.

Chúng ta tiếp tục thay đổi chương trình một chút để hiểu hơn về hàm init.

Thay đổi dòng code var rectLen, rectWidth float64 = 6, 7 trong file geometry.go thành var rectLen, rectWidth float64 = -6, 7. Ở đây chúng ta đã khởi tạo rectLen là giá trị âm.

Chạy ứng dụng, bạn sẽ thấy kết quả:

rectangle package initialized  
main package initialized  
2017/04/04 00:28:20 length is less than zero

Package rectangle vẫn được khởi tạo như bình thường, tiếp theo sau là các biến toàn cục rectLen và rectWidth thuộc package main. rectLen là giá trị âm. Do đó khi hàm init chạy, chương trình sẽ kết thúc sau khi in ra câu length is less than zero.

Tải code tại github

 

Sử dụng blank identifier (định danh trống)

Đối với golang, khi chúng ta import một package mà không sử dụng thì sẽ bị xem là không hợp lệ. Trình biên dịch sẽ báo lỗi nếu bạn làm như vậy. Lý do của việc này là để tránh trường hợp dư thừa các package không sử dụng, vì điều đó sẽ làm tăng đáng kể thời gian biên dịch. Ta thử thay đổi code trong file geometry.go như sau:

//geometry.go
package main 

import (   

     "geometry/rectangle" //import package tùy chỉnh

)
func main() {

}

Chạy chương trình sẽ ném ra lỗi geometry.go:6: imported and not used: "geometry/rectangle"

Tuy nhiên trường hợp chúng ta muốn import trước một số package trong khi đang phát triển ứng dụng và sẽ sử dụng các package đó sau mà không dùng ngay bây giờ thì sao. Blank identifier (định danh trống) sẽ giúp chúng ta trong những trường hợp đó, ký hiệu là dấu _

Lỗi của chương trình trên có thể được bỏ qua bởi câu lệnh sau:

package main

import (  
    "geometry/rectangle" 
)

var _ = rectangle.Area //error silencer (bỏ qua lỗi)

func main() {

}

Câu lệnh var _ = rectangle.Area giúp bỏ qua lỗi này. Chúng ta nên chú ý đến những câu lệnh bỏ qua lỗi và xóa chúng cùng với những package không được sử dụng sau khi hoàn thành chương trình. Cách viết lệnh bỏ qua lỗi ngay sau khi khai báo import package được khuyến khích.

Đôi khi chúng ta cần phải import một package chỉ để đảm bảo việc khởi tạo được diễn ra mặc dù chúng ta không cần sử dụng bất kỳ hàm nào hoặc biến nào từ package đó. Ví dụ, chúng ta cần đảm bảo rằng hàm init của package rectangle được gọi mặc dù ta không cần sử dụng đến package này. Blank identifier _ có thể được dùng đến trong trường hợp này.

package main 

import (   

     _ "geometry/rectangle" 

)
func main() {

}

Khi chạy chương trình trên, ta có output rectangle package initialized. Chúng ta đã khởi tạo thành công package rectangle mà không sử dụng đến nó.

Phần hướng dẫn về packages đến đây là kết thúc. Xin vui lòng để lại những ý kiến và phản hồi của các bạn. Cảm ơn vì đã đọc.

Bài viết được dịch từ golangbot

Phần tiếp theo - if else statement