Golang đại pháp – 2.3 Khai báo biến
Go
50
golang
49
White

nhaancs viết ngày 10/01/2018

Bài viết được đăng tải đầu tiên tại: http://nhaancs.com/golang-dai-phap-2-3-khai-bao-bien/

golang dai phap

Khai báo với từ khoá var tạo ra 1 biến có kiểu xác định, gán cho biến đó 1 cái tên và khởi tạo giá trị ban đầu cho nó. Khai báo biến có dạng như sau:

var tênbiến kiểudữliệu = giátrịkhởitạo

Chúng ta có thể bỏ qua một trong hai thành phần là kiểu dữ liệu hoặc giá trị khởi tạo. Nếu kiểu dữ liệu bị bỏ qua, nó sẽ được xác định bằng kiểu của giá trị khởi tạo. Nếu giá trị khởi tạo bị bỏ qua, thì biến sẽ được gán giá trị zero của kiểu dữ liệu, 0 đối với kiểu số, false đối với kiểu boolean, "" đối với kiểu stringnil cho interface và các kiểu liên kết (slice, con trỏ, map, channel, function). Giá trị zero của một kiểu tổng hợp như array hoặc struct có tất cả các phần tử hay field của nó có giá trị zero.

Giá trị zero đảm bảo rằng biến luôn giữ một giá trị xác định thuôc kiểu được khai báo, trong Go không có khái niệm biến chưa được khởi tạo. Điều này làm cho code đơn giản hơn và tránh xảy ra lỗi dùng biến chưa khởi tạo mà không cần làm thêm các bước kiểm tra. Ví dụ sau sẽ in ra chuỗi rỗng thay vì gây ra lỗi:

var s string
fmt.Println(s) // ""

Có thể khai báo và khởi tạo nhiều biến cùng lúc.

// Khai báo nhiều biến cùng kiểu:
var x, y, z int 
// int, int, int

// Khai báo nhiều biến khác kiểu:
var a, b, c = "", 1, false // string, int, boolean

Giá trị khởi tạo có thể là một giá trị hoặc một biểu thức. Các biến cấp độ package được khởi tạo trước khi hàm main chạy, các biến local được khởi tạo khi hàm khai báo nó chạy đến dòng khai báo trong quá trình chạy hàm.

Có thể khởi tạo nhiều biến cùng lúc bằng cách hứng nhiều giá trị trả về cùng lúc của một function:

var f, err = os.Open(name) // os.Open trả về gí trị file và error 

2.3.1 Khai báo biến kiểu ngắn gọn

Bên trong function, có một cách thay thế để khai báo và khởi tạo biến là dùng :=:

tênbiến := giátrịkhởitạo 

Giá trị khởi tạo được xác định bởi giá trị khởi tạo. Ví dụ:

anim := gif.GIF{LoopCount: nframes}
freq := randrand.Float64() * 3.0
t := 0.0

Cách khai báo này khá ngắn gọn và linh hoạt nên thường được dùng để khai báo các biến local. Khai báo biến với var được dùng khi kiểu của biến khác với kiểu của giá trị khởi tạo, hoặc khi giá trị khởi tạo không quan trọng:

i := 100 // int 
var boiling float64 = 100 // float64
var name []string 
var err error 
var p Point

Bạn cũng có thể khai báo nhiều biến cùng lúc nhưng chú ý là nên dùng khi nó khiến chương trình dễ đọc hơn, hay dùng trong phần khởi tạo giá trị của vòng lặp for:

i, j := 0, ""

Cần lưu ý là := (khai báo biến) khác với = (phép gán), các lập trình viên thường hay nhầm lẫn giữa hay ký hiệu này, đặc biệt là khi khai báo nhiều biến:

i, j = j, i // trao đổi giá trị giữa i và j 

:= hay được dùng để khai báo biến và hứng giá trị trả về từ một lời gọi hàm:

f, err := os.Open(name)
if err != nill {
    return err
}
// --dùng biến f ở đây--
f.Close()

Khi khai báo biến một cách ngắn gọn thì danh sách biến bên trái := không nhất thiết phải là khai báo mới hoàn toàn, nếu biến nào đã được khai báo trước đó rồi (trong cùng scope) thì := sẽ có tác dụng giống như phép gán đối với biến đó.

Trong đoạn code bên dưới, câu lệnh đầu tiên khai báo cả hai biến inerr. Câu lệnh thứ hai chỉ khai báo out và gán giá trị vào biến err có sẵn (đã được khai báo bên trên).

in, err := os.Open(infile)
//...
out, err := os.Create(outfile)

Nhưng khi khai báo với := thì phải có ít nhất một biến được khai báo mới, nếu không thì trình biên dịch sẽ báo lỗi:

file, err := os.Open(infile)
//...
file, err := os.Create(outfile) // error, không có biến nào được khai báo mới hết

Fix:

file, err := os.Open(infile)
//...
file, err = os.Create(outfile) // chuyển sang phép gán, không báo lỗi nữa

2.3.2 Con trỏ (pointer)

Về bản chất, biến là 1 phần của bộ nhớ có thể lưu trữ giá trị. Biến được khai báo và xác định bằng tên biến, như x, nhưng có nhiều biến được xác định bằng biểu thức như: x[i] hay x.f. Tất cả biểu thức này lấy giá trị lưu trữ của biến, trừ trường hợp nó nằm bên trái toán tử gán, trường hợp này giá trị mới sẽ được gán cho biến.

Giá trị của con trỏ chính là địa chỉ của biến trong bộ nhớ, vì vậy có thể nói con trỏ lưu trữ địa chỉ của một giá trị trong bộ nhớ. Không phải tất cả các giá trị đều có địa chỉ nhưng tất cả các biến đều có địa chỉ. Với con trỏ, bạn có thể đọc và cập nhật giá trị của biến một cách gián tiếp, không cần phải biết tên biến.

Nếu biến được khai báo như sau: var x int thì biểu thức &x ("địa chỉ của x") trả về con trỏ tới biến kiểu integer, con trỏ đó có kiểu là *int (con trỏ của int). Nếu con trỏ được gán vào p thì ta có thể nói p trỏ tới x hoặc p chứa địa chỉ của x. p chứa 1 địa chỉ trỏ tới tới 1 biến, để lấy ra giá trị chứa tại địa chỉ đó ta dùng *p. *p tương với biến x vì vậy nó có thể nằm bên trái của toán tử gán trong trường hợp cập nhật giá trị biến.

x := 1
p := &x // p có kiểu *int, trỏ tới x
fmt.Println(*p) // 1
*p = 2 // giống như x = 2
fmt.Println(x) // 2

Mỗi thành phần của kiểu tổng hợp (mỗi phần tử của array hay mỗi field của struct, ...) cũng là 1 biến và cũng có địa chỉ.

Giá trị zero của con trỏ thuộc bất cứ kiểu nào là nil. p != nil trả về true nếu p đang trỏ tới một biến nào đó. Bạn có thể so sánh con trỏ, hai con trỏ bằng nhau khi và chỉ khi chúng cùng trỏ tới 1 biến, hoặc cùng có giá trị nil.

var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // true false false 

Bạn có thể trả về địa chỉ của 1 biến local trong function. Trong ví dụ bên dưới, biến local v được tạo khi gọi hàm f vẫn tiếp tục tồn tại ngay cả khi f đã chạy xong và trả về về giá trị bởi vì lúc này con trỏ p đang trỏ tới v:

var p = f()

func f() *int {
    v := 1
    return &v
}

Mỗi lần chạy f trả về một giá trị khác nhau:

fmt.Println(f() == f()) // false

Bởi vì con trỏ chứa địa chỉ của biến, vì vậy khi truyền con trỏ vào một hàm thì hàm đó có thể cập nhật giá trị của biến 1 cách gián tiếp thông qua con trỏ được truyền vào. Ví dụ bên dưới hàm incr tăng giá trị của biến mà con trỏ truyền vào trỏ tới lên 1 và trả về giá trị sau khi cập nhật:

func incr(p *int) int {
    *p++ // tăng giá trị của biến mà p trỏ tới lên 1

    return *p
}

v := 1
incr(&v) // v bây giờ bằng  2
fmt.Println(incr(&v)) // 3 - và v bây giờ bằng 3 

Mỗi khi lấy ra địa chỉ của biến hay copy con trỏ tức là chúng ta đã tạo ra 1 alias hay 1 cách khác để định danh cùng 1 biến. Ví dụ, *p là alias của v. Pointer giúp chúng ta truy cập tới biến mà không cần dùng tên của biến, nhưng nó cũng là con dao hai lưỡi: khi cần tìm các câu lệnh liên quan tới 1 biến nào đó, bạn cần phải biết hết tất cả các alias của biến đó. Không phải chỉ có con trỏ mới tạo ra alias, copy value của các kiểu liên kết như khi copy slice, map, channel hay struct, array, interface chứa các kiểu trên cũng tạo ra alias (thực ra chúng ta chỉ copy 1 địa chỉ trỏ tới các giá trị này).

Con trỏ là thành phần quan trọng trong package flag, dùng các tham số command-line để thiết lập giá trị cho các biến trong chương trình. Để minh hoạ, phiên bản tiếp theo của chương trình echo nhận 2 cờ tuỳ chọn: -n cho phép echo bỏ qua ký tự xuống dòng cuối cùng, và -s dùng để xác định ký tự ngăn cách khi xuất kết quả ra màn hình. File echo4.go:

package main 

import (
    "flag"
    "fmt"
    "strings"
)

var n = flag.Bool("n", false, "bỏ qua ký tự xuống dòng")
var sep = flag.String("s", " ", "Chuỗi ngăn cách kết quả")

func main() {
    flag.Parse()
    fmt.PrintPrint(strings.Join(flag.Args(), *sep))
    if !*n {
        fmt.Println()
    }
}

Hàm flag.Bool tạo ra một cờ (flag) kiểu boolean, nó nhận vào 3 tham số: tên của flag ("n"), giá trị mặc định (false) và tin nhắn sẽ được hiển thị khi use cung cấp sai tham số, sai cờ hay dùng flag -h hoặc -help, tương tự với flag.String. nsep chỉ là con trỏ tới flag nên phải truy cập gián tiếp *n, *sep.

Khi chương trình chạy, phải gọi flag.Parse() để cập nhật giá trị cho các flag, trước khi có thể được sử dụng. Các tham số không phải flag sẽ được liệt kê với hàm flag.Args() (trả về slice string). Nếu khi chạy hàm flag.Parse xảy ra lỗi thì nó sẽ in một câu thông báo và gọi os.Exit(2) để dừng chương trình. Hãy chạy thử một vài trường hợp:

$ ./echo4 abc def ghk
abc def ghk

$ ./echo4 -s / abc def ghk
abc/def/ghk

$ ./echo4 -n  abc def ghk
abc def ghk$

& ./echo4 -help 
Usage of ./echo4:
  -n    bỏ qua ký tự xuống dòng
  -s string
        Chuỗi ngăn cách kết quả (default " ")

2.3.3 Hàm new

Một cách khác để tạo biến là dùng hàm new. Biểu thức new(T) tạo ra một biến vô danh kiểu T, khởi tạo giá trị zero kiểu T và trả về con trỏ của biến vô danh đó (kiểu *T).

p := new(int) // p, pointer kiểu *int, trỏ tới biến vô danh kiểu int 
fmt.Println(*p) // 0
*p = 2 // gán biến vô danh bằng 2 
fmt.Println(*p) // 0 

Biến tạo với hàm new không khác gì so với biến tạo bình thường ngoại trừ chúng ta không cần đặt cho nó 1 cái tên và new(T) có thể dùng trong biểu thức. new chỉ là cú pháp rút gọn (syntactic sugar) không phải hàm cơ bản. Hai hàm bên dưới là giống nhau:

func newInt() *int {
    return new(Int)
}

func newInt2() *int {
    var dummy int
    return &dummy 
}

Mỗi lần gọi new trả về 1 địa chỉ khác nhau:

p := new(Int)
q := new(Int)
fmt.Println(p == q) // false

Có một ngoại lệ, nếu 2 biến có kiểu không chứa thông tin gì như struct{}, hay [0]int thì tuỳ trường hợp có thể có cùng địa chỉ.

new là một fucntion được khai báo sẵn, không phải từ khoá nên bạn có thể khai báo lại new với 1 mục đích khác:

func delta(old, new int) {
    return new - old 
}

Tất nhiên là trong hàm delta bạn không thể dùng hàm new có sẵn vì bây giờ bạn đã dùng tên đó với mục đích khác.

2.3.4 Dòng đời của biến

Dòng đời của biến là khoảng thời gian nó tồn tại trong lúc chương trình được thực thi. Vòng đời của biến cấp độ package là toàn bộ thời gian thực thi của chương trình. Ngược lại các biến local có dòng đời động: mỗi khi một dòng khai báo biến được thực thi thì có một biến được tạo mới và tồn tại cho đến khi biến đó trở thành unreachable (không còn được sử dụng), khi đó bộ nhớ lưu trữ nó sẽ được giải phóng (garbage colection).

Các tham số và kết quả hàm cũng là biến local được tạo ra mỗi khi hàm khai báo nó chạy. Ví dụ trong hàm lissajous có đoạn:

for t = 0.0; t < cycles*2*math.Pi; t += res {
    x := math.Sin(t)
    y := math.Sin(t*freq + phase)
    img.SetColorIndex(size + int(x*size + 0.5) + int(y*size + 0.5), blackIndex)
}

Biến t được tạo mới trước vòng lặp for trong khi hai biến x, y được tạo trong mỗi lần lặp.

Làm sao Go biết khi nào cần giải phóng bộ nhớ của biến (garbage colection)? Chuyện dài lắm, mình sẽ nói sau nha. Còn bây giờ các bạn cứ hiểu cơ bản là mỗi biến cấp độ package, và biến local của mỗi function đang chạy có thể là khởi đầu của của các liên kết trỏ đến nó (từ các con trỏ hay các liên kết khác), nếu không có liên kết nào trỏ tới nó nữa thì nó trở thành unreachable và không còn ảnh hưởng tới các tính toán trong phần còn lại của chương trình.

Bởi vì dòng đời của biến được xác định bằng các xem biến đó có còn reachable (cò được sử dụng) hay không, vì vậy một biến local có thể tồn tại sau mỗi vòng lặp hay sau khi kết thúc function.

Trình biên dịch có thể chọn lưu trữ biến trên stack hay heap nhưng lựa chọn đó không dựa vào việc bạn dùng var hay new để khai báo biến.

var global *int 

func f() {
    var int x
    x = 1
    global = &x
}

func g() {
    y := new(int)
    *y = 1
}

Ở đây biến x được lưu trong heap vì nó vẫn còn reachable từ biến global sau khi hàm f return, ta gọi x trốn thoát (escape) khỏi f. Ngược lại khi g return *y trở nên unreachable và bộ nhớ chứa nó sẽ được giải phóng. Khi *y không escape khỏi g thì trình biên dịch có thể lưu *y trên stack mặc dù nó được tạo bằng hàm new. Mặc dù việc lưu trữ biến trên heap hay stack có thể không phải là vấn đề bạn quan tâm nhưng với số lượng lớn nó có thể ảnh hưởng tới hiệu năng chương trình. Luôn nhớ rằng khi biến escape khỏi hàm thì có nghĩa là nó sẽ là chương trình tốn thêm 1 lượng bộ nhớ.

Garbage colection giúp chúng ta rất nhiều trong việc quản lý bộ nhớ, nhưng nó không giúp bạn có thể hoàn toàn không lo lắng gì về bộ nhớ. Bạn không cần phải lưu trữ và giải phóng bộ nhớ một cách thủ công, nhưng bạn phải hiểu về vòng đời của biến để đảm bảo hiệu năng của chương trình.

Reference: The Go Programming Language

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

nhaancs

11 bài viết.
4 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Bài viết liên quan
White
9 2
Makefile thực hiện một số thao tác thường dùng trong Go Khi làm project Go mình thường tạo một file Makefile dạng này: Lưu ý nhớ thay thành tên m...
Huy Trần viết 2 năm trước
9 2
White
43 16
Go là gì? Dùng nó cho việc gì? Chắc hẳn đến thời điểm hiện tại, không ai là chưa nghe đến Go (hay còn gọi là Golang), một ngôn ngữ lập trình được ...
Huy Trần viết 3 năm trước
43 16
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
11 bài viết.
4 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!