Author Archives: Phở Code

Go – Struct và Interface

Trong phần này chúng ta sẽ tìm hiểu về khái niệm struct và interface trong Go. Đây là các khái niệm trong lập trình hướng đối tượng (OOP), nếu bạn chưa từng làm việc với OOP thì bạn nên tham khảo thêm vì lý thuyết OOP trên mạng.

Giả sử chúng ta có đoạn code tính diện tích hình tròn và diện tích hình chữ nhật từ các điểm trên mặt phẳng như sau:

package main

import (
    "fmt"
    "math"
)

func distance(x1, y1, x2, y2 float64) float64 {
    a := x2 - x1
    b := y2 - y1
    return math.Sqrt(a*a + b*b)
}

func rectangleArea(x1, y1, x2, y2 float64) float 64 {
    l := distance(x1, y1, x2, y2)
    w := distance(x1, y1, x2, y1)
    return l * w
}

func circleArea(x, y, r float64) float64 {
    return math.Pi * r*r;
}

func main() {
    var rx1, ry1 float64 = 0, 0
    var rx2, ry2 float64 = 10, 10
    var cx, cy, cr float64 = 0, 0, 5

    fmt.Println(rectangleArea(rx1, ry1, rx2, ry2))
    fmt.Println(circleArea(cx, cy, cr))
}

Thoạt nhìn thì bài toán này có vẻ đơn giản, tuy nhiên khi bạn muốn tính diện tích của khoảng vài chục hình vuông/hình tròn, lúc này bản thân việc đặt tên biến đã muốn mệt chứ chưa nói đến việc tính toán, dĩ nhiên bạn có thể dùng array, slice… nhưng giả sử bạn muốn hình dung trong đầu hình nào ở vị trí số mấy trong mảng là cũng khá khó khăn rồi. Do đó bạn cần dùng kiểu struct để có thể quản lý tốt hơn.

Struct

Một struct là một kiểu dữ liệu đặc biệt, kiểu này chứa biến thuộc các kiểu dữ liệu khác, các biến ở đây thường được gọi là các trường hoặc các thuộc tính… Ví dụ chúng ta định nghĩa struct có tên Circle (hình tròn) và Rectangle (hình chữ nhật) như sau:

type Circle struct {
    x float64
    y float64
    z float64
}

type Rectangle struct {
    x1 float64
    y1 float64
    x2 float64
    y2 float64
}

Để định nghĩa một struct thì chúng ta dùng từ khóa type, từ khóa này báo cho Go biết là chúng ta đang định nghĩa một kiểu dữ liệu mới, theo sau là tên kiểu dữ liệu do chúng ta tự đặt, tiếp theo là từ khóa struct để báo cho Go biết là chúng ta đang định nghĩa một struct, cuối cùng là danh sách các trường của struct này. Mỗi trường chúng ta khai báo gồm tên trường và tên kiểu dữ liệu. Ngoài ra chúng ta có thể khai báo ngắn gọn lại như sau:

type Circle struct {
    x, y, r float64 
}

type Rectangle struct {
    x1, y1, x2, y2 float64
}

Khai báo biến kiểu struct

Chúng ta khai báo biến kiểu struct giống như khai báo một biến bình thường:

var c Circle
var r Rectangle

Lúc này biến c có kiểu dữ liệu là Circle, các trường của biến c sẽ có kiểu dữ liệu mặc định là 0 với int, 0.0 với float, “” với string, nil với con trỏ, biến r cũng tương tự như vậy.  Hoặc khai báo bằng cách dùng hàm new():

c := new(Circle)
r := new(Rectangle)

Nếu muốn khởi tạo và gán giá trị cho các trường luôn thì chúng ta làm như sau:

c := Circle{x: 0, y : 0, r : 5}
r := Rectangle{x1 : 0, y1 : 10, x2 : 0, y2 : 10}

Hoặc chúng ta không cần ghi tên trường ra nhưng phải truyền kiểu dữ liệu theo đúng thứ tự đã định nghĩa:

c := Circle{0, 0, 5}
r := Rectangle{0, 0, 10, 10}

Trường

Để thao tác các trường thì chúng ta dùng dấu chấm “.” như sau:

fmt.Println(c.x, c.y, c.z)
c.x = 10
c.y = 5

Định nghĩa struct trong một hàm cũng giống như các kiểu dữ liệu khác:

func circleArea(c Circle) float64 {
    return math.Pi * c.r*c.r
}

func rectangleArea(r Rectangle) float64 {
    l := distance(r.x1, r.y1, r.x1, r.y2)
    w := distance(r.x1, r.y1, r.x2, r.y1)
    return l * w
}

Khi gọi hàm chúng ta cũng chỉ truyền tên biến struct vào là được:

c := Circle{0, 0, 5}
r := Rectangle{0, 0, 10, 10}
fmt.Println(circleArea(c))
fmt.println(rectangleArea(r))

Cũng giống như các kiểu dữ liệu khác, khi truyền một struct vào hàm, thực chất Go sẽ sao chép biến đó vào trong tham số của hàm chứ không trực tiếp thao tác với hàm, do đó nếu muốn hàm thực hiện các thao tác trên chính struct được truyền vào thì chúng ta phải truyền con trỏ (hoặc địa chỉ bộ nhớ của biến) vào hàm bằng cách dùng phép toán &:

func circleArea(c *Circle) float64 {
    return math.Pi * c.r*c.r
}

func main() {
    c := Circle{0, 0, 5}
    fmt.Println(circleArea(&c))
}

Phương thức

Phương thức là các hàm của riêng một struct, khi dùng thì chỉ có các biến có kiểu struct đó mới gọi được hàm. Ví dụ:

func (c *Circle) area() float64 {
    return math.Pi * c.r*c.r
}

func (r *Rectangle) area() float64 {
    l := distance(r.x1, r.y1, r.x1, r.y2)
    w := distance(r.x1, r.y1, r.x2, r.y1)
    return l * w
}

Để định nghĩa một phương thức thì ở giữa từ khóa func và tên hàm chúng ta khai báo struct sở hữu phương thức đó theo cú pháp giống như khai báo tham số của hàm. Để gọi một phương thức của một struct thì chúng ta cũng dùng dấu chấm “.” như sau:

fmt.Println(c.area())
fmt.Println(r.area())

Một phương thức của một struct có thể đọc giá trị của các trường trong struct đó, do đó dùng struct sẽ làm cho việc code trở nên dễ dàng hơn rất nhiều, chúng ta không còn phải truyền các biến không cần thiết vào hàm nữa, và hay hơn là không phải truyền con trỏ. Ngoài ra phương thức của một struct chỉ có struct đó mới dùng được nên việc đặt tên cũng dễ dàng hơn nhiều, thay vì đặt tên circleArea()rectangleArea(), chúng ta chỉ cần đặt là area() là đủ.

Interface

Trong các ví dụ trên, chúng ta đã định nghĩa 2 struct là Rectangle (hình chữ nhật) và Circle (hình tròn), cả 2 struct này đều một phương thức tính diện tích có tên giống nhau là area(). Chúng ta có thể “gộp chung” 2 phương thức đó vào một kiểu dữ liệu khác có tên là Interface:

type Shape interface {
    area() float64
}

Trong ví dụ trên chúng ta định nghĩa một Interface có tên Shape, interface này có một phương thức là area(). Để định nghĩa một interface thì cũng giống như định nghĩa một struct, chúng ta dùng từ khóa type, tiếp đến là tên interface rồi đến từ khóa interface, sau đó là danh sách các phương thức trong cặp dấu ngoặc nhọn {}.

Interface thực ra cũng không hẳn là một kiểu dữ liệu như struct vì interface chỉ chứa các phương thức chứ không chứa các trường, interface cũng không có phần định nghĩa phương thức ở ngoài như các struct, chúng chỉ chứa tên phương thức là hết. Vậy thì việc sử dụng interface có gì hay? Câu trả lời là chúng ta có thể dùng interface để thực hiện tính toán trên nhiều kiểu struct khác nhau mà không quan tâm các struct đó là gì. Ví dụ:

package main 

import (
    "fmt"
    "math"
)

type Shape interface {
    area() float64
}

type Circle struct {
    x, y, r float64
}

type Rectangle struct {
    x1, y1, x2, y2 float64
}

func distance(x1, y1, x2, y2 float64) float64 {
    a := x2 - x1
    b := y2 - y1
    return math.Sqrt(a*a + b*b)
}

func (c *Circle) area() float64 {
    return math.Pi * c.r*c.r
}

func (r *Rectangle) area() float64 {
    l := distance(r.x1, r.y1, r.x1, r.y2)
    w := distance(r.x1, r.y1, r.x2, r.y1)
    return l * w
}

func totalArea(shapes ...Shape) float64 {
    var area float64
    for _, s := range shapes {
    area += s.area()
    }
    return area
}

func main() {
    c := Circle{0, 0, 5}
    r := Rectangle{0, 0, 10, 10}
 
    fmt.Println(totalArea(&c, &r))
}

Trong ví dụ trên, chúng ta định nghĩa hàm totalArea() có chức năng tính tổng diện tích của bất cứ hình nào, hàm này nhận vào tham số là kiểu Shape, nhưng chúng ta có thể truyền vào kiểu Circle hoặc kiểu Rectangle đều được, nếu chúng ta truyền vào kiểu Circle, khi gọi phương thức area() thì Go sẽ gọi phương thức area() của struct Circle, và ngược lại khi truyền vào Rectangle thì gọi phương thức area() của Rectangle.

178.53981633974485

Django – Host website với PythonAnywhere

Từ trước tới nay chúng ta chỉ làm việc trên máy local, tức là máy của chính chúng ta. Trong bài này chúng ta sẽ tìm cách triển khai website đó lên mạng để cả thế giới có thể truy cập được 🙂

Trên mạng có rất nhiều nhà cung cấp dịch vụ host, ở đây chúng ta sẽ sử dụng nhà cung cấp PythonAnywhere vì PythonAnywhere miễn phí dịch vụ cho các website nhỏ lẻ, ít có khách truy cập, thích hợp cho việc test host.

Ngoài ra chúng ta sẽ sử dụng một dịch vụ khác nữa là GitHub, đây là dịch vụ lưu trữ code. Ngoài GitHub còn có rất nhiều dịch vụ lưu trữ code khác, tuy nhiên GitHub khá phổ biến, hầu hết các coder trên thế giới đều có tài khoản GitHub, nếu chưa có thì bạn cũng tạo ngay một cái đi 🙂

Về cơ bản thì bạn sẽ code trang web của bạn trên máy của bạn, sau đó đẩy code lên GitHub rồi lấy code đó xuống PythonAnywhere để hiển thị.

Git

Git là một “hệ thống quản lý phiên bản” được sử dụng bởi rất nhiều coder trên toàn thế giới. Git cho phép theo dõi sự thay đổi của các file theo thời gian để chúng ta có thể xem lại bất cứ lúc nào, trong Microsoft Word cũng có tính năng này nhưng của Git mạnh hơn nhiều.

Bạn có thể tải Git cho Windows về và cài đặt tại địa chỉ https://git-scm.com/. Khi cài đặt bạn nếu không hiểu thì có thể để các thiết lập mặc định là được rồi.

Git theo dõi sự thay đổi của các file trong các repository (viết tắt là repo). Để tạo một repo thì chúng ta mở Command Prompt (cmd), chuyển đến thư mục mà bạn lưu code của website trong đó, rồi gõ các lệnh sau:

C:\PhoCode>git init
Initialized empty Git repository in C:/PhoCode/.git/
C:\PhoCode>git config --global user.name "Pho Code"
C:\PhoCode>git config --global user.email "phocode7@gmail.com"

Bạn lưu ý đặt usernameemail theo ý bạn.

Việc tạo repository chỉ cần thực hiện một lần (tức là sau này chúng ta không cần khai báo username và email nữa). Git sẽ theo dõi sự thay đổi của các file và thư mục trong thư mục này, tuy nhiên sẽ có một số file mà chúng ta muốn bỏ qua. Để làm việc này thì chúng ta tạo một file có tên .gitignore trong thư mục đó. Bạn mở một trình editor (như notepad) lên rồi gõ đoạn sau vào:

*.pyc
__pycache__
myvenv
db.sqlite3
/static
.DS_Store

Lưu ý dấu chấm trong .gitignore rất quan trọng, nếu bạn không lưu tên file như thế được thì dùng Save As…

Nếu trong quá trình làm web mà bạn dùng cơ sở dữ liệu SQLite thì trong thư mục gốc của website sẽ có file db.sqlite3, chúng ta đưa file này vào danh sách trong .gitignore vì trên PythonAnywhere không dùng SQLite.

Tiếp theo chúng ta tạo một ứng dụng Django bình thường rồi đưa tất cả vào trong thư mục của repository. Ví dụ mình có một trang web như sau:

Capture1Capture

(Source: w3schools)

Chúng ta sẽ sử dụng lệnh git add để cập nhật lại nội dung trong repository. Thông thường chúng ta nên dùng lệnh git status trước khi dùng git add để xem mọi sự thay đổi trong repo có đúng như mình đã làm không. Chẳng hạn như bạn tạo file sai, thiếu hoặc thừa file/thư mục…v.v

C:\PhoCode>git status
On branch master

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    .gitignore
    PhoCode/
    blog/
    manage.py

nothing added to commit but untracked files present (use "git add" to track)

Sau khi đã kiểm tra xong thì chúng ta dùng lệnh addcommit để lưu lại như sau:

C:\PhoCode>git add --all
  [...]
C:\PhoCode>git commit -m "Pho Code site, first commit"
  [...]

Đẩy code lên trên GitHub

Nếu bạn chưa có tài khoản GitHub thì lên github.com tạo một cái.

Sau đó chúng ta cũng tạo một repo trên GitHub với tên tùy ý. Khi tạo thì bỏ check phần Initialize this repository with a README, phần Add .gitignore chọn None, phần Add a license chọn None.

Capture2

Sau khi tạo xong, bạn sẽ được cung cấp URL của repo dưới dạng HTTPS hoặc SSH. Ở đây chúng ta dùng HTTPS, copy dòng đó lại.

Capture

Việc tiếp theo là kết nối repo trên GitHub với repo trên máy của chúng ta bằng cách gõ 2 lệnh sau:

C:\PhoCode>git remote add origin https://github.com/phocode/phocode-site.git
C:\PhoCode>git push -u origin master
...

Tiếp theo bạn nhập username và mật khẩu của tài khoản GitHub là xong, lệnh git remote sẽ kết nối repo trên GitHub với repo trong máy bạn, lệnh git push sẽ đẩy code trong máy bạn lên GitHub, bây giờ code của bạn là nằm ở repo trên GitHub.

Lấy code xuống PythonAnywhere

Đầu tiên bạn vào www.pythonanywhere.com tạo một tài khoản Beginner (miễn phí). Sau đó chúng ta đăng nhập và sẽ được chuyển đến trang bảng điều khiển (dashboard), ở tab Consoles, bạn click dòng Bash để mở trình terminal tại phần Start a new console:

Capture

Màn hình terminal sẽ hiện lên và bạn sẽ thao tác với host thông qua màn hình này. Lưu ý là PythonAnywhere chạy trên nền Linux nên nếu bạn sử dụng Windows thì sẽ thấy hơi khác một tí. Tuy nhiên những câu lệnh của các phần mềm bên thứ 3 (như git) thì vẫn như cũ.

Chúng ta tiến hành lấy code từ GitHub xuống bằng cách dùng lệnh git clone:

Capture

Lệnh git clone sẽ tạo một thư mục có tên giống với repo trên tài khoản GitHub của bạn, trong thư mục này sẽ chứa các file và thư mục mà bạn đã đẩy lên đó từ trước. Bạn có thể dùng lệnh ls để xem:

$ cd phocode.site
$ ls
PhoCode blog manage.py

Tiếp theo chúng ta cần cài Django với phiên bản mà chúng ta đang dùng vào host trên PythonAnywhere, tuy nhiên chúng ta không cài như thường mà sẽ cài vào môi trường ảo, môi trường ảo cho phép chúng ta chạy các chương trình hay các gói Python ở các phiên bản khác nhau để tránh vấn đề xung đột (ví dụ website X chạy trên Django 1.8, trong khi website Y chạy Django 1.9…v.v).

Đầu tiên chúng ta tạo một môi trường ảo với Python 3.5 như sau:

$ virtualenv --python=python3.5 phocode_env

Đoạn code trên sẽ tạo một thư mục với tên phocode_env chứa các thư viện và các công cụ cần thiết, đây là một môi trường Python ảo sử dụng Python 3.5.

Tiếp theo chúng ta yêu cầu sử dụng môi trường này bằng cách gõ lệnh sau:

$ source phocode_env/bin/activate

Cuối cùng chúng ta cài đặt Django với phiên bản mà website của bạn đang sử dụng vào môi trường này bằng trình pip:

(phocode_env)$ pip install django==1.9.4
[...]

Tạo cơ sở dữ liệu trên PythonAnywhere

Như đã nói ở trên, PythonAnywhere có thể sử dụng cơ sở dữ liệu khác với cơ sở dữ liệu mà bạn dùng, nên việc tiếp theo bạn cần làm là tạo cơ sở dữ liệu trên PythonAnywhere như sau:

(phocode_env)$ python manage.py migrate
System check identified some issues:
[...]

Tạo web app

Chúng ta đã lấy được code, có cơ sở dữ liệu và có môi trường ảo. Việc cần làm tiếp theo là tạo một Web App trên PythonAnywhere.

Chúng ta thoát khỏi màn hình terminal (bằng cách bấm vào logo của PythonAnywhere), sau đó vào tab Web, click Add a new web app. Một hộp thoại mở lên thông báo cho bạn biết do chúng ta đang dùng bản miễn phí nên trang web sẽ có tên miền là <username>.pythonanywhere.com (ngoài ra web app này chỉ có thời gian hiệu lực là 3 tháng), click Next, tiếp theo chúng ta chọn Manual Configuration (chứ không chọn Django), tiếp theo chọn phiên bản Python 3.5 rồi click Next và đợi.

Tiếp theo bạn sẽ được chuyển đến trang cấu hình web app, chúng ta sẽ chỉ định web app sử dụng môi trường ảo mà chúng ta đã tạo ra hồi nãy. Trong phần VirtualEnv, click vào dòng Enter path to a virtualenv, if desired, rồi điền vào đường dẫn đến thư mục chứa môi trường ảo mà chúng ta đã tạo, thường là /home/<PythonAnywhere-username>/phocode-site/phocode_env.

Capture

 

 

 

 

Nếu bạn không biết đường dẫn chính xác là gì hoặc muốn chắc ăn, bạn có thể mở trình terminal lên, dùng lệnh cd để chuyển đến thư mục đó rồi dùng lệnh pwd để biết đường dẫn chính xác.

Capture1

Sau đó click vào dấu tick màu xanh để lưu đường dẫn môi trường ảo.

Cấu hình file WSGI

Django sử dụng giao thức WSGI, đây là chuẩn hiển thị web của Python. Khi bạn tạo một project Django trên máy thì bạn sẽ có một file tên wsgi.py.

Trong tab Web, chúng ta sửa file WSGI bằng cách click vào dòng /var/www/<PythonAnywhere-username>_pythonanywhere_com_wsgi.py trong phần Code. Chúng ta xóa toàn bộ nội dung file đó và thay bằng đoạn code sau:

import os
import sys

path = '/home/<PythonAnywhere-username>/phocode-site'
if path not in sys.path:
 sys.path.append(path)

os.environ['DJANGO_SETTINGS_MODULE'] = 'PhoCode.settings'

from django.core.wsgi import get_wsgi_application
from django.contrib.staticfiles.handlers import StaticFilesHandler
application = StaticFilesHandler(get_wsgi_application())

Lưu ý biến path là đường dẫn tới thư mục gốc chứa code của bạn, biến os.environ có phần đầu là tên thư mục chứa file settings.py. 

Lớp StaticFilesHandler chịu trách nhiệm việc tải các file tĩnh (như CSS, Javascript…) từ các site khác về.

Thế là xong, bây giờ bạn về lại tab Web, click vào nút Reload <PythonAnywhere-username>.pythonanywhere.com màu xanh lá cây rồi đợi, sau đó bạn có thể nhập đường dẫn đó vào trình duyệt để xem website của mình được rồi.

Capture

Nếu có bất kì lỗi gì xảy ra mà trang web không thể hiển thị hay hiển thị sai, bạn có thể xem thông báo lỗi trong file error.log, bạn có thể mở tìm thấy file này ở tab Web, phần Log files. Một số lỗi thường xảy ra là do:

  • Quên tạo môi trường ảo, quên kích hoạt môi trường ảo, quên cài Django vào môi trường ảo, quên tạo lại cơ sở dữ liệu.
  • Đường dẫn đến thư mục môi trường ảo thiết lập trong tab Web bị sai.
  • Thiết lập file WSGI sai
  • Phiên bản Python trong khi tạo môi trường ảo và phiên bản Python trong Web App không trùng khớp.

Go – Con trỏ

Trong phần này chúng ta sẽ tìm hiểu một khái niệm quan trọng đó là con trỏ.

Khi chúng ta gọi một hàm và truyền tham số vào đó, giá trị của tham số đó sẽ được sao chép vào trong hàm đó, ví dụ:

package main

import "fmt"

func zero(x int) {
    x = 0
}

func main() {
    x := 5
    zero(x)
    fmt.Println(x)
}

Đoạn code trên sẽ cho kết quả là 5.

Trong đoạn code trên, hàm zero() có tác dụng gán cho biến x giá trị là 0. Trong hàm main() chúng ta khai báo một biến x có giá trị là 5 rồi truyền vào trong hàm zero(), sau đó chúng ta in giá trị của biến x ra màn hình, kết quả vẫn bằng 5. Lý do là vì giá trị của biến x trong hàm main() được sao chép vào tham số x của riêng hàm zero() chứ hàm zero() không nhận một biến x nào cả.

Tuy nhiên nếu chúng ta muốn hàm zero() thao tác trực tiếp luôn với biến x của hàm main() thì chúng ta phải dùng đến con trỏ. Ví dụ:

package main

import "fmt"

func zero(xPtr *int) {
    *xPtr = 0
}

func main() {
    x := 5
    zero(&x)
    fmt.Println(x)  
}

Con trỏ là một loại biến đặc biệt, được dùng để lưu trữ địa chỉ của biến khác trong bộ nhớ RAM chứ không lưu trữ một giá trị cụ thể nào. Để khai báo một biến con trỏ thì chúng ta thêm dấu sao "*" vào trước tên kiểu dữ liệu. Khi chúng ta in giá trị của một biến con trỏ ra màn hình thì giá trị đó sẽ là một số ở hệ hexa (hệ 16), đó là địa chỉ bộ nhớ mà con trỏ này đang “trỏ” tới.

Khi gán giá trị cho biến con trỏ, chúng ta cũng phải đưa vào đó một địa chỉ bộ nhớ nào đó chứ không đưa một giá trị vào, để lấy địa chỉ bộ nhớ của một biến thì chúng ta dùng dấu "&" trước tên biến.

Ngoài chức năng khai báo biến con trỏ, dấu "*" còn có tác dụng lấy giá trị của địa chỉ bộ nhớ mà con trỏ đang tham chiếu tới, ngược lại chúng ta cũng có thể gán giá trị cho địa chỉ đó thông qua dấu "*".

Ví dụ:

package main

import "fmt"

func main() {
    var x *int 
    var y int
    y = 0
    x = &y
 
    fmt.Println(x)
    fmt.Println(&y)
    fmt.Println(*x)
    fmt.Println(y)
 
    *x = 1
 
    fmt.Println(*x)
    fmt.Println(y)
}

Trong đoạn code trên, chúng ta :

  • Khai báo x là biến con trỏ kiểu int, y là một biến int bình thường.
  • Gán giá trị cho y là 0
  • Cho x trỏ tới địa chỉ bộ nhớ của y
  • Lúc này sẽ mang giá trị là địa chỉ bộ nhớ của y, chúng ta có thể dùng dấu & để lấy địa chỉ bộ nhớ của y, hoặc dùng dấu * để lấy giá trị tại địa chỉ của y (hay giá trị cửa chính biến y)
  • Gán giá trị cho y (hay giá trị tại địa chỉ bộ nhớ mà x đang tham chiếu tới) là 1 bằng cách dùng dấu *
0xc0420044f8
0xc0420044f8
0
0
1
1

Phần Output ở máy bạn có thể khác vì địa chỉ bộ nhớ mỗi lần hệ điều hành cung cấp cho biến là khác nhau.

New

Ngoài cách khai báo biến con trỏ bằng dấu “*”, Go còn cho phép chúng ta khai báo biến con trỏ bằng cách dùng hàm new()Ví dụ:

package main

import "fmt"

func one(xPtr *int) {
    *xPtr = 1
}

func main() {
    xPtr := new(int)
    one(xPtr)
    fmt.Println(*xPtr)
}

Tham số mà hàm new() nhận là tên của một kiểu dữ liệu.

Nếu bạn đã từng làm việc với C/C++ thì bạn biết là sau khi khai báo một biến con trỏ, trước khi kết thúc chương trình chúng ta phải dùng lệnh delete để trả biến đó về cho hệ điều hành, nếu không hệ điều hành sẽ “nghĩ” rằng biến đó vẫn còn đang được sử dụng (nhưng thực chất là không) nên sẽ không cấp cho các ứng dụng khác, dẫn đến lãng phí bộ nhớ. Trong Go thì chúng ta không phải làm việc này, Go sẽ tự động dọn dẹp mọi thứ trước khi chương trình kết thúc.

Go – Hàm

Hàm là một khối lệnh độc lập có chức năng nhận dữ liệu, xử lý và trả về kết quả. Trong các phần trước chúng ta chỉ làm việc với một hàm duy nhất là hàm main(), trong phần này chúng ta sẽ làm việc với nhiều hàm khác.

Khai báo hàm

Giả sử chúng ta có đoạn code tính giá trị trung bình của một slice như sau:

package main

import "fmt"

func main() {

    xs := []float64{98, 93, 77, 82, 83}
    total := 0.0
    for _, v := range xs {
        total += v
    }
    fmt.Println(total / float64(len(xs))
}

Bài toán tính giá trị trung bình của một dãy số là rất phổ biến, do đó chúng ta nên định nghĩa hàm riêng để thực hiện công việc này. Ví dụ:

package main

import "fmt"

func average(input []float64) float64 {
    total := 0.0
    for _, v := range input {
        total += v
    }
    return total / float64(len(input))
}

func main() {
    xs := []float64{98, 93, 77, 82, 83}
    fmt.Println(average(xs))
}

Như trong đoạn code ví dụ trên, để khai báo một hàm thì chúng ta dùng từ khóa func, tiếp theo là tên hàm, rồi danh sách các tham số đầu vào trong cặp dấu ngoặc tròn (), rồi đến kiểu dữ liệu trả về, cuối cùng là phần thân hàm nằm trong cặp dấu ngoặc nhọn {}. Tham số đầu vào và kiểu dữ liệu trả về có thể không có cũng được.

Trong đoạn code trên chúng ta định nghĩa hàm average() có kiểu trả về là float64, hàm này nhận một tham số đầu vào là biến xs. Trong phần thân hàm chúng ta thay câu lệnh fmt.Println() thành câu lệnh return, câu lệnh return có chức năng kết thúc hàm và “trả về” một giá trị cho hàm đã gọi nó, để gọi một hàm thì chúng ta chỉ đơn giản là ghi tên hàm đó ra rồi đưa tham số vào, ở đây chúng ta gọi hàm average() trong câu lệnh fmt.Println(average(xs)) trong hàm main(). Cả 2 đoạn code trên đều cho ra kết quả giống nhau.

Có một số lưu ý như sau:

  • Một hàm không thể đọc một biến được định nghĩa trong hàm khác, ví dụ:
func f() {
    fmt.Println(x)
}

func main() {
    x := 5
    f() 
}

Đoạn code trên sẽ báo lỗi, để hàm f() có thể đọc được biến x thì chúng ta phải truyền x vào làm tham số như sau:

func f(x int) {
    fmt.Println(x)
}

func main() {
    x := 5
    f(x)
}
  • Hàm được gọi chồng lên nhau, ví dụ chúng ta có đoạn code sau:
func main() {
    fmt.Println(f1())
}

func f1() int {
    return f2() 
}

func f2() int {
    return 1
}

Trong hàm main() chúng ta gọi hàm f1(), hàm f1() lại gọi hàm f2(), hàm f2() sẽ trả về 1 cho hàm f1(), hàm f1() lại trả về giá trị 1 đó cho hàm main(). Bạn có thể hình dung quá trình đó như sau:

Capture

  • Chúng ta có thể đặt tên cho giá trị trả về, ví dụ:
func f2() (r int) {
    r = 1
    return
}

Trả về nhiều giá trị

Go cho phép một hàm được trả về nhiều giá trị. Ví dụ:

func f() (int, int) {
    return 5, 6
}

func main() {
   x, y := f()
}

Chúng ta khai báo danh sách các kiểu trả về trong cặp dấu ngoặc tròn (), ngăn cách nhau bởi dấu phẩy ",", phía sau câu lệnh return cũng là danh sách các giá trị trả về cách nhau bởi dấu phẩy ",", chúng ta cũng gán nhiều giá trị cho nhiều biến cách nhau bởi dấu phẩy ",".

Thông thường chúng ta thường trả về một giá trị của một kết quả tính toán nào đó, và một giá trị lỗi cho biết công việc của hàm có thành công hay không.

Tùy biến số lượng tham số

Tham số cuối cùng của một hàm có thể được khai báo theo dạng đặc biệt như sau:

package main

import "fmt"

func add(args ...int) int {
    total := 0
    for _, v := range args {
        total += v
    } 
    return total
}

func main() {
    fmt.Println(add(1, 2, 3))
}

Chúng ta thêm 3 dấu chấm "..." vào trước tên kiểu dữ liệu của tham số cuối cùng, Go sẽ hiểu rằng tham số này có thể có 0 hoặc nhiều giá trị được truyền vào, khi gọi hàm chúng ta có thể truyền vào 0 hoặc nhiều giá trị, ngăn cách nhau bởi dấu phẩy, đặc tính này cho phép hàm nhận tham số một cách linh hoạt hơn.

Hàm Closure

Chúng ta có thể định nghĩa một hàm bên trong một hàm khác. Ví dụ:

package main

import "fmt" 

func main() {
    add := func(x, y int) int {
        return x + y
    }
    fmt.Println(add(1, 1))
}

Trong đoạn code trên chúng ta khai báo biến add có kiểu func(int, int) int. Các hàm được định nghĩa kiểu này có thể đọc được các biến nằm cùng hàm với nó, ví dụ:

package main

import "fmt"

func main() {
    x := 0
    increment := func() int {
        x++
        return x
    }
    fmt.Println(increment())
    fmt.Println(increment())
}

Trong đoạn code trên, increment thực hiện tăng biến x lên một đơn vị, mặc dù x được khai báo ngoài hàm increment nhưng hàm này vẫn có thể đọc được.

Đệ quy

Một hàm có thể gọi chính nó, ví dụ:

func factorial(x uint) uint {
    if x == 0 {
        return 1
    }
    return x * factorial(x - 1)
}

Khi một hàm gọi chính nó thì đây là kỹ thuật lập trình đệ quy.

Hàm Closure và đệ quy là các kỹ thuật lập trình cao cấp tạo nên mô hình lập trình chức năng. Hầu hết người mới học sẽ thấy hơi khó hiểu cách hoạt động của chúng so với việc dùng cách câu lệnh bình thường như for, if….

Defer, Panic và Recover

Lệnh defer có tác dụng chạy một lệnh khác sau khi một hàm đã kết thúc. Ví dụ:

package main

import "fmt"

func first() {
    fmt.Println("1st")
}

func second() {
    fmt.Println("2nd")
}

func main() {
    defer second()
    first()
}

Trong ví dụ trên, hàm first() sẽ thực hiện đầu tiên, sau đó đến hàm second() mặc dù hàm second() đứng trước, lý do là bởi vì chúng ta thêm từ khóa defer vào trước second(), do đó hàm second() sẽ được thực hiện cuối cùng khi tất cả các công việc khác đã hoàn tất.

Thường chúng ta dùng defer cho các công việc dọn dẹp tài nguyên.

Hàm panic dùng để phát sinh lỗi, ví dụ:

package main

func main() {
    panic("Co loi xay ra") 
}

Đoạn code trên sẽ cho kết quả như sau:

panic: Co loi xay ra

goroutine 1 [running]:
panic(0x456e20, 0xc0420040b0)
    C:/Go/src/runtime/panic.go:500 +0x1af
main.main()
    C:/main.go:4 +0x74
exit status 2

Chẳng hạn như khi chúng ta viết chương trình tính phép chia mà người dùng nhập vào mẫu số là 0 thì chúng ta có thể dùng hàm panic để báo lỗi. Tuy nhiên hàm panic() ngoài việc thông báo lỗi sẽ dừng chương trình luôn, do vậy Go đưa ra hàm recover(), hàm recover() có tác dụng khôi phục chương trình và trả về tham số đã được truyền vào trong hàm panic(). Ví dụ:

package main

import "fmt"

func main() {
    panic("Co loi xay ra")
    str := recover()
    fmt.Println(str)
}

Tuy nhiên đoạn code trên sẽ không chạy vì hàm panic() đã dừng hoàn toàn chương trình trước khi chúng ta gọi hàm recover(). Do đó để dùng hàm recover() chúng ta phải bọc trong lệnh defer như sau:

package main

import "fmt"

func main() {
    defer func() {
        str := recover()
        fmt.Println(str)
    }()
    panic("Co loi xay ra")
}

Kết quả:

Co loi xay ra

Go – Array, Slice và Map

Trong phần này chúng ta sẽ tìm hiểu thêm về các kiểu dữ liệu có sẵn trong Go là array, slice và map.

Array

Array (hay mảng) là một tập hợp các phần tử có cùng kiểu dữ liệu nằm liên tiếp nhau. Chúng ta khai báo một array trong Go như sau:

var x [5]int

Trong đó x là tên array, có 5 phần tử có kiểu dữ liệu là int. Giả sử chúng ta có đoạn code chương trình như sau:

package main

import "fmt"

func main() {
    var x [5]int
    x[4] = 100
    fmt.Println(x)
}

Đoạn code trên sẽ in ra kết quả như sau:

[0 0 0 0 100]

Dòng x[4] = 100 có nghĩa là gán giá trị 100 cho phần tử thứ 5 trong mảng, các phần tử không được gán giá trị sẽ có giá trị mặc định là 0. Lý do phần tử đó là phần tử thứ 5 chứ không phải thứ 4 là vì các phần tử trong array được đánh số thứ tự từ 0.

Để truy xuất một phần tử của mảng chúng ta cũng sử dụng toán tử []. Ví dụ:

package main

import "fmt"

func main() {
    var x [5]float64
    x[0] = 98
    x[1] = 93
    x[2] = 77
    x[3] = 82
    x[4] = 83

    var total float64 = 0
    for i := 0 ; i < 5 ; i++ {
         total = total + x[i]
    }
    fmt.Println(total / 5)
}

Đoạn code trên tính giá trị trung bình của các phần tử trong mảng. Khi chạy chương trình sẽ in kết quả ra 86.6.

Tuy nhiên chương trình này viết “chưa hay”, nếu chúng ta thay đổi số lượng phần tử từ 5 thành 6 hay 7… thì cứ mỗi lần sửa chúng ta phải thay hai biểu thức i<5 total/5 thành i<6, total/6…v.v Để thuận tiện hơn thì chúng ta có thể sử dụng hàm len(), hàm này sẽ trả về số lượng phần tử có trong mảng:

var total float64 = 0
for i := 0 ; i < len(x) ; i++ {
    total = total + x[i]
}
fmt.Println(total / len(x))

Tuy nhiên đoạn code trên sẽ báo lỗi:

invalid operation: total / 5
(mismatched types float 64 and int)

Lỗi này có nghĩa là biểu thức total / len(x) là không hợp lệ vì total có kiểu dữ liệu float64 còn len(x) lại trả về một kiểu int. Để khắc phục thì chúng ta có thể chuyển đổi kiểu dữ liệu của len(x) sang float64 như sau:

fmt.Println(total / float64(len(x)))

Ngoài ra Go còn cho phép chúng ta dùng vòng lặp for theo cú pháp rút gọn như sau khi duyệt mảng:

var total float64 = 0

for i, value := range x {
    total = total + value
}
fmt.Println(total / float64(len(x)))

Trong cú pháp trên, biến i chứa vị trí của phần tử hiện tại mà nó tham chiếu đến trong mỗi lần lặp qua mảng, value chứa dữ liệu của vị trí theo biến i, tiếp theo là từ khóa range cùng với tên mảng được sử dụng.

Đoạn code trên sẽ báo lỗi như sau:

i declared and not used

Dòng báo lỗi trên có nghĩa là biến i đã được khai báo nhưng không được sử dụng. Trong các ngôn ngữ khác thì hầu như trình biên dịch chỉ cảnh báo chứ không báo lỗi khi chúng ta khai báo biến mà không dùng tới, nhưng trong Go thì khác, vì nhà phát triển muốn code trở nên tối ưu hơn nên chúng ta không được khai báo biến mà không dùng.

Để khắc phục thì chúng ta có thể thay tên biến thành dấu gạch dưới _ như sau:

var total float64 = 0
for _, value := range x {
    total += value
}
fmt.Println(total / float64(len(x)))

Trình biên dịch sẽ hiểu rằng đây là một biến “giả” được tạo ra nhưng không được sử dụng.

Ngoài ra chúng ta còn có thể khai báo array nhanh như sau:

x := [5]float64{ 98, 93, 77, 82, 83 }

Slice

Slice cũng là một kiểu dữ liệu dạng tập hợp như array, các phần tử trong slice cũng được đánh chỉ số. Điểm khác biệt giữa slice và array là số phần tử trong slice có thể thay đổi được. Chúng ta khai báo một slice như sau:

var x []float64

Dòng trên sẽ tạo một slice có 0 phần tử. Ngoài ra chúng ta có thể tạo một slice bằng hàm make() như sau:

x := make([]float64, 5)

Dòng trên sẽ tạo một slice có 5 phần tử.

Chúng ta có thể tạo slice từ một array bằng cách dùng biểu thức [low:high] như sau:

arr := [5]float64{98, 93, 77, 82, 83}
x := arr[0:5]

Slice x sẽ có giá trị là các phần tử của mảng arr, trong đó [low:high] tức là lấy các phần tử từ vị trí low đến vị trí high - 1. Chẳng hạn như [0:5] sẽ trả về các phần tử [98, 93, 77, 82, 83], [1: 4] trả về các phần tử [93, 77, 82].

Ngoài ra các vị trí low, high cũng có thể bỏ đi, chẳng hạn như arr[:] sẽ lấy toàn bộ array, arr[0:] sẽ lấy các phần tử từ vị trí 0 đến vị trí cuối cùng, arr[:5] sẽ lấy các phần tử từ 0 đến 4.

Ngoài ra trong còn Go có 2 hàm hỗ trợ làm việc với slice là append()copy(). Ví dụ:

package main

import "fmt"

func main() {
    slice1 := []int{1, 2, 3}
    slice2 := append(slice1, 4, 5)
    fmt.Println(slice1, slice2)
}

Hàm append() trong dòng slice2 := append(slice1, 4, 5) có tác dụng tạo một slice mới có các phần tử giống như slice1 và thêm vào 2 phần tử mang giá trị 4 và 5, tức slice2 sẽ có các phần tử là [1, 2, 3, 4, 5].

Ví dụ đối với hàm copy():

package main

import "fmt"

func main() {
    slice1 := []int{1, 2, 3}
    slice2 := make([]int, 2)
    copy(slice2, slice1)
    fmt.Println(slice1, slice2)
}

Dòng copy(slice2, slice1) sẽ sao chép các phần tử trong slice1 vào slice2, tuy nhiên khi tạo slice2 chỉ có 2 phần tử nên hàm này chỉ sao chép 2 phần tử đầu tiên của slice1 vào slice2, do đó slice2 sẽ có các phần tử là [1, 2].

Map

Map cũng là kiểu dữ liệu dạng tập hợp nhưng các phần tử trong map không có thứ tự, tức là chúng ta không thể truy xuất các phần tử theo chỉ số như slice với array. Thay vào đó, các phần tử trong map là các cặp khóa-giá trị, trong các ngôn ngữ khác thì chúng còn có cái tên như mảng liên kết, bảng băm, từ điển… Việc truy xuất các phần tử trong map được thực hiện thông qua khóa.

Trong Go chúng ta khai báo một map như sau:

var x map[string]int

Chúng ta dùng từ khóa map, sau đó là kiểu dữ liệu của khóa trong cặp dấu ngoặc vuông [], rồi đến kiểu dữ liệu của giá trị. Trong dòng code trên mỗi phần tử trong map x có khóa kiểu string mang giá trị kiểu int.

Hoặc chúng ta có thể tạo một map bằng cách dùng hàm make:

x := make(map[string]int)

Việc gán giá trị và truy xuất giá trị trong map cũng giống với array và slice, chỉ khác là thay vì dùng số thì bây giờ chúng ta dùng khóa. Ví dụ:

package main

import "fmt"

func main() {
    x := make(map[string]int)
    x["key"] = 10
    fmt.Println(x["key"])
}

Đoạn code trên sẽ in số 10 ra màn hình.

Chúng ta có thể xóa một phần tử trong map bằng hàm delete():

delete(x, "key")

Tham số cho hàm delete() gồm có tên map và khóa cần xóa.

Thực chất khi truy xuất một phần tử của map chúng ta nhận được 2 giá trị trả về chứ không chỉ có giá trị của khóa, giá trị thứ 2 là một giá trị kiểu boolean cho biết việc truy xuất có thành công hay không. Nếu chúng ta truy xuất một khóa có tồn tại trong map thì giá trị boolean trả về true, ngược lại trả về false, ví dụ:

package main

import "fmt"

func main() {
    x := make(map[string]int)
    x["key"] = 10
 
    value, ok := x["key"]
    fmt.Println(value, ok)
  
    value2, ok2 := x["key2"]
    fmt.Println(value2, ok2)
}

Trong đoạn code trên, map x không có khóa key2, khi truy xuất giá trị sẽ trả về 0, ngoài ra giá trị boolean sẽ là false. Còn khóa key có tồn tại nên sẽ trả về số 10 và giá trị boolean của nó là true. Ngoài ra ở đây chúng ta còn biết được là Go cho phép chúng ta gán nhiều giá trị vào nhiều biến trên một dòng thông qua dấu phẩy ",".

10 true
0 false

Giống như array, chúng ta có thể vừa khai báo vừa gán giá trị cho map như sau:

elements := map[string]string{
    "H": "Hydrogen",
    "He": "Helium",
    "Li": "Lithium",
    "Be" : "Beryllium",
    "B" : "Boron",
    "C" : "Carbon",
    "N" : "Nitrogen",
    "O" : "Oxygen",
    "F" : "Fluorine",
    "Ne" : "Neon",
}

Lưu ý là sau phần tử cuối cùng vẫn có dấu phẩy “,” vì Go muốn giúp chúng ta khi cần comment một dòng thì không phải mất công xóa dấu phẩy ở dòng trước:

"F" : "Fluorine",
//"Ne" : "Neon",

Chúng ta còn có thể lồng các map vào nhau, tức là mỗi phần tử của map sẽ là một map khác, ví dụ:

elements := make(map[string]map[string]string) {
        "H" : map[string]string {
        "name" : "Hydrogen",
    },
        "He" : map[string]string {
        "name" : "Helium",
    },
        "Li" : map[string]string {
        "name" : "Lithium",
    },
        "Be" : map[string]string {
        "name" : "Beryllium",
    },
        "B" : map[string]string {
         "name" : "Boron",
    },
        "C" : map[string]string {
        "name" : "Carbon",
    },
        "N" : map[string]string {
        "name" : "Nitrogen",
    },
        "O" : map[string]string {
        "name" : "Oxygen",
    },
        "F" : map[string]string {
        "name" : "Fluorine",
    },
        "Ne" : map[string]string {
        "name" : "Neon",
    }, 
} 

Trong đoạn code trên, elements là một map, mỗi phần tử của elements là một map khác.

Go – Lệnh điều khiển

Trong phần này chúng ta sẽ tìm hiểu về các lệnh điều khiển chương trình là for, ifswitch.

Lệnh for

Giả sử chúng ta cần in các con số từ 1 đến 10 ra màn hình, chúng ta có thể ghi 10 câu lệnh fmt.Println() như sau:

package main

import "fmt"

func main() {
    fmt.Println(1)
    fmt.Println(2)
    fmt.Println(3)
    fmt.Println(4)
    fmt.Println(5)
    fmt.Println(6)
    fmt.Println(7)
    fmt.Println(8)
    fmt.Println(9)
    fmt.Println(10)
}

Lệnh for cho phép lặp đi lặp lại các câu lệnh nhiều lần. Ví dụ:

package main

import "fmt"

func main() {
    var i int = 1
    for i <= 10 {
        fmt.Println(i)
        i  = i + 1
    }
}

Đoạn code trên sẽ in các chữ số từ 1 đến 10 ra màn hình, thay vì phải dùng 10 câu lệnh fmt.Println() thì bây giờ chúng ta chỉ cần dùng câu lệnh for là đủ.

Trong đoạn code trên, chúng ta khai báo biến i và gán giá trị là 1. Sau đó chúng ta sử dụng lệnh for để chạy câu lệnh fmt.Println() 10 lần bằng cách ghi từ khóa for, theo sau là một biểu thức điều kiện i <= 10, rồi tới khối lệnh nằm trong cặp dấu ngoặc nhọn {}. 

Khi câu lệnh for bắt đầu chạy, đầu tiên câu lệnh này kiểm tra xem biến i có giá trị bé hơn 10 hay không, nếu đúng thì thực hiện những câu lệnh bên dưới, rồi quay ngược lại kiểm tra i cho đến khi nào i không còn bé hơn 10 thì dừng lại. Do đó chúng ta đặt lệnh i = i + 1 bên trong vòng lặp, cứ mỗi lần lặp giá trị của biến i sẽ được tăng lên 1 cho đến khi i = 10 thì vòng lặp for sẽ thoát.

1
2
3
4
5
6
7
8
9
10

Chúng ta có thể đưa câu lệnh khai báo biến và lệnh tăng giá trị của biến ngay trên một dòng như sau:

func main() {
    for i := 1; i <= 10 ; i++ {
       fmt.Println(i)
    }
}

Hâu hết các ngôn ngữ khác có rất nhiều lệnh lặp như while, do, until, foreach.... nhưng Go chỉ hỗ trợ một lệnh lặp duy nhất là lệnh for.

Lệnh if

Bây giờ chúng ta thử in các con số từ 1 đến 10 và cho biết số đó là chẵn hay lẻ. Để làm điều này thì chúng ta sẽ cần dùng đến câu lệnh if. Ví dụ:

package main

import "fmt"

func main() {
    for i := 1; i <= 10 ; i++ {
        if i % 2 == 0 {
            fmt.Println(i, "chan")
        } else {
            fmt.Println(i, "le")
        }
    }
}

Ở đây chúng ta sử dụng câu lệnh if để kiểm tra xem biến i mang giá trị là số chẵn hay lẻ. Chúng ta ghi câu lệnh if, theo sau là một biểu thức điều kiện (tức là kết quả phải là true hoặc false), rồi đến khối lệnh nằm trong cặp dấu ngoặc nhọn {}, sau đó chúng ta có thể có thêm các câu lệnh else if hoặc else.

Nếu biểu thức điều kiện phía sau iftrue thì thực hiện câu khối lệnh phía sau nó, ngược lại thì tiếp tục kiểm tra các biểu thức điều kiện tiếp theo nếu có.

Ở đây biểu thức điều kiện là câu lệnh i % 2 == 0, tức là chúng ta kiểm tra xem i có chia hết cho 2 hay không (hay i chia cho 2 không dư), nếu đúng thì i là số chẵn.

1 le
2 chan
3 le
4 chan
5 le
6 chan
7 le
8 chan
9 le
10 chan

Lệnh switch

Giả sử chúng ta muốn in các chữ số bằng chữ, chúng ta có thể viết đoạn code như sau:

if i == 0 {
    fmt.Println("Khong")
} else if i == 1 {
    fmt.Println("Mot")
} else if i == 2 {
    fmt.Println("Hai")
} else if i == 3 {
    fmt.Println("Ba")
} else if i == 4 {
    fmt.Println("Bon")
} else if i == 5 {
    fmt.Println("Nam")
}

Thay vì dùng câu lệnh if như trên, chúng ta có thể dùng câu lệnh switch như sau:

switch i {
case 0:  fmt.Println("Khong")
case 1:  fmt.Println("Mot")
case 2:  fmt.Println("Hai")
case 3:  fmt.Println("Ba")
case 4:  fmt.Println("Bon")
case 5:  fmt.Println("Nam")
default: fmt.Println("Khong biet")
}

Chúng ta dùng từ khóa switch, theo sau là một biểu thức điều kiện, rồi tới một danh sách các từ khóa case, ứng với mỗi từ khóa case là một giá trị nào đó, rồi tới dấu 2 chấm : và các lệnh sẽ được thực hiện.

Ý nghĩa của lệnh switch là, nếu biểu thức điều kiện ở switch cho kết quả trùng với giá trị ở từ khóa case nào thì thực hiện các câu lệnh sau từ khóa case đó. Ngoài ra ở đây chúng ta còn có từ khóa default, có tác dụng thực hiện các câu lệnh nếu giá trị ở switch không trùng với bất kì từ khóa case nào.

Go – Biến

Trong phần này chúng ta sẽ tìm hiểu về biến trong Go.

Biến

Biến là nơi lưu trữ dữ liệu, một biến gồm có 2 phần là tên biến và kiểu dữ liệu. Ví dụ:

package main

import "fmt"

func main() {
    var x string = "Hello World"
    fmt.Println(x)
}

Đoạn code chương trình trên in ra dòng chữ Hello World như trong các bài trước, nhưng thay vì chúng ta đưa chuỗi Hello World trực tiếp vào trong hàm Println() thì ở đây chúng ta gán vào một biến có tên là x.

Để khai báo một biến trong Go thì chúng ta dùng từ khóa var, theo sau là tên biến, rồi đến kiểu dữ liệu, cuối cùng chúng ta có thể gán giá trị cho biến hoặc gán sau cũng được. Ví dụ:

package main

import "fmt"

func main() {
    var x string
    x  = "Hello World"
    fmt.Println(x);
}

Biến trong Go nói riêng hay lập trình nói chung thì cũng tương đương như biến mà chúng ta được học trong toán, tuy nhiên có một điểm hơi khác đó là biến trong lập trình thì có thể thay đổi giá trị được, ví dụ:

package main

import "fmt"

func main() {
    var x string
    x = "first"
    fmt.Println(x)
    x = "second"
    fmt.Println(x)
}

Cũng vì thế cách phát biểu về một giá trị của biến cũng khác đi. Chẳng hạn như khi gặp dòng x="Hello World", bạn có thể nói “x bằng Hello World”, tuy nhiên nói đúng hơn là “x nhận giá trị Hello World”, hoặc “x được gán giá trị Hello World”.

Ngoài ra thường trong lập trình chúng ta hay khai báo một biến rồi sau đó gán giá trị cho nó luôn nên Go cho phép chúng ta khai báo và gán giá trị nhanh như sau:

x := "Hello World"

Ở đây chúng ta không dùng từ khóa var, không khai báo kiểu dữ liệu, thay vào đó chúng ta ghi tên biến rồi dùng toán tử := theo sau là giá trị để khai báo nhanh một biến và gán giá trị ngay tại chỗ. Trình biên dịch Go sẽ tự động nhận diển kiểu dữ liệu dựa vào giá trị mà bạn gán cho biến. Chẳng hạn như ở đây Go thấy giá trị là “Hello World”, tức là một string nên sẽ tự động cho biến x kiểu dữ liệu string. Tóm lại 2 dòng sau đây có chức năng y hệt nhau:

var x string = "Hello World"

x := "Hello World"

Đặt tên biến

Tên biến có thể có một hoặc nhiều kí tự, có thể chứa chữ cái, dấu gạch dưới _ và chữ số. Kí tự đầu tiên phải là chữ cái hoặc dấu gạch dưới.

Go không quan tâm bạn đặt tên biến như thế nào, nên khi đặt thì chúng ta nên đặt sao cho dễ nhớ và dễ hiểu. Ví dụ:

title := "Pho Code"

fmt.Println("Website name: ", title)

Phạm vi hoạt động của biến

Chúng ta viết lại chương trình Hello World ở trên như sau:

package main

import "fmt"

var x string = "Hello World"

func main() {
    fmt.Println(x)
}

Ở đây chúng ta đưa dòng khai báo biến x ra ngoài hàm main(). Làm như thế biến x sẽ có thể truy cập bởi bất kì hàm nào. Ví dụ:

var x string = "Hello World"

func main() {
    fmt.Println(x)
}

func f() {
    fmt.Println(x)
}

Trong đoạn code trên hàm f() có thể đọc được giá trị của biến x. Giả sử chúng ta đưa biến x vào lại bên trong hàm main như sau:

func main() {
    var x string = "Hello World"
    fmt.Println(x)
}

func f() {
    fmt.Println(x)
}

Đoạn code trên khi biên dịch sẽ báo lỗi như sau:

main.go:11: undefined: x

Dòng báo lỗi trên có nghĩa là biến x không tồn tại. Bởi vì nó chỉ được khai báo ở trong hàm main() nên chỉ có thể đọc được ở trong hàm main(). Chính xác hơn là một biến chỉ có thể được đọc ở trong cặp dấu ngoặc nhọn gần nhất {} theo tài liệu của Go.

Hằng số

Hằng số đơn giản chỉ là các biến không thể thay đổi dược giá trị. Cách khai báo và gán giá trị cho hằng số cũng giống như với biến, chỉ khác một chỗ là thay từ khóa var bằng từ khóa const. Ví dụ:

package main

import "fmt"

func main() {
    const x string = "Hello World"
    fmt.Println(x)
}

Chúng ta không thể thay đổi giá trị của hằng số.

const x string = "Hello World"

x = "Goodbye World"

Đoạn code trên sẽ báo lỗi như sau:

main.go:7: cannot assign to x

Hằng số thường được dùng để lưu các giá trị dùng nhiều lần mà không cần phải khai báo lại. Trong Go có rất nhiều hằng số được tạo sẵn, ví dụ như hằng Pi trong gói math.

Khai báo nhiều biến

Ngoài cách khai báo từng biến trên một dòng, bạn có thể khai báo nhiều biến một lúc như sau:

var (
    a = 5
    b = 10
    c = 15
)

Chúng ta dùng từ khóa var (hoặc const), theo sau là cặp dấu ngoặc tròn (), rồi tới danh sách các biến và giá trị của chúng.

Go – Kiểu dữ liệu

Trong phần trước chúng ta đã sử dụng kiểu dữ liệu string cho chuỗi Hello World. Kiểu dữ liệu phân nhóm các giá trị có liên quan với nhau, có các thao tác có thể thực hiện trên chúng và cách chúng được lưu trữ.

Kiểu dữ liệu trong Go là tĩnh, tức là không thể thay đổi được không giống như một số ngôn ngữ như PHP, Javascript… cho phép thay đổi kiểu dữ liệu trong suốt quá trình chạy.

Trong phần này chúng ta sẽ tìm hiểu về một số kiểu dữ liệu cơ bản trong Go là kiểu số nguyên, số thực, kiểu string và kiểu boolean.

Số nguyên

Đây là các giá trị số nguyên giống hoàn toàn như trong toán, tuy nhiên số nguyên trong máy tính có giới hạn (tức không có giá trị nào là vô cùng ∞ cả). Các kiểu số nguyên trong Go là uint8, uint16, uint32, uint64, int8, int16, int32, int64. Các con số 8, 16, 32, 64 có nghĩa là máy tính cần dùng bao nhiêu bit để biểu diễn số nguyên đó. uint tức là unsigned int – là kiểu số nguyên không âm. Bảng dưới đây cho biết giới hạn của từng loại kiểu số nguyên:

KIỂU GIỚI HẠN
uint8 0 – 255
uint16 0 – 65535
uint32 0 – 4294967295
uint64 0 – 18446744073709551615
int8 -128 – 127
int16 -32768 – 32767
int32 -2147483648 – 2147483647
int64 -9223372036854775808 – 9223372036854775807

Kiểu uint8 còn có tên khác là byte, kiểu int32 có tên khác là rune. 

Đặc biệt trong Go còn có 3 kiểu số nguyên phụ thuộc hệ điều hành là uint, intuintptr, 3 kiểu dữ liệu này có giới hạn giống như kiến trúc của hệ điều hành mà bạn đang sử dụng. Ví dụ nếu bạn đang dùng Windows 64 bit thì kiểu int sẽ có giới hạn giống như kiểu uint64. Thông thường khi sử dụng số nguyên bạn dùng int là đủ rồi. Ví dụ:

package main

import "fmt"

func main() {
    fmt.Println("1 + 1 = ", 1 + 1)
}

Kết quả:

1 + 1 = 2

Số thực

Đây là các giá trị số có phần thập phân, ví dụ 1.234, 123.4… Việc lưu trữ cũng như thao tác với số thực trong máy tính khá phức tạp nên chúng ta cũng không đi sâu vào làm gì. Ở đây chúng ta chỉ có một số lưu ý như sau:

  1. Số thực không bao giờ chính xác một cách tuyệt đối, rất khó để biểu diễn chính xác một số thực. Ví dụ như phép trừ 1.01 0.99 sẽ cho ra kết quả là 0.020000000000000018 chứ không phải là 0.02 như bạn vẫn nghĩ.
  2. Cũng giống như số nguyên, số thực trong máy tính cũng có nhiều giới hạn khác nhau.

Trong Go có 2 kiểu số thực là float32float64, 2 kiểu số phức là complex64complex128. Thông thường để biểu diễn số thực, bạn chỉ cần dùng float64 là đủ.

String

String (chuỗi) là các kí tự được bọc trong cặp dấu nháy kép hoặc nháy đơn được dùng để biểu diễn văn bản. String nằm trong dấu nháy kép có thể sử dụng các kí tự điều khiển đặc biệt như \n là xuống dòng, \t là dấu tab.

Chúng ta có thể thực hiện một số thao tác thường dùng trên String như tính độ dài chuỗi, lấy kí tự tại vị trí nhất định, nối chuỗi. Ví dụ:

package main

import "fmt"

func main() {
    fmt.Println(len("Hello World"))
    fmt.Println("Hello World"[1])
    fmt.Println("Hello " + "World")
} 

Dấu cách cũng được tính là một kí tự, do đó chiều dài chuỗi “Hello World” là 11.

Các kí tự trong một chuỗi được đánh số thứ tự từ 0. Câu lệnh “Hello World”[1] sẽ cho ra kết quả là kí tự ở vị trí số 2, tuy nhiên nếu bạn chạy đoạn code trên thì kết quả sẽ cho ra là số 101 chứ không phải kí tự ‘e’, là bởi vì 101 là mã ASCII của kí tự ‘e’

Chúng ta có thể dùng phép toán + lên 2 chuỗi, kết quả là một chuỗi mới được nối từ 2 chuỗi đầu.

11
101
Hello World

Boolean

Các giá trị boolean là các giá trị 1 bit, có 2 giá trị là truefalse được dùng để biểu diễn ý nghĩa ĐÚNG hoặc SAI. Trong Go có 3 phép toán có thể thao tác với giá trị boolean là && (phép AND), || (phép OR) ! (phép NOT). 

Bảng dưới đây mô tả cách thực hiện phép toán với kiểu boolean:

Phép AND:

BIỂU THỨC GIÁ TRỊ
true && true true
true && false false
false && true false
false && false false

Phép OR:

BIỂU THỨC GIÁ TRỊ
true || true true
true || false true
false || true true
false || false false

Phép NOT:

BIỂU THỨC GIÁ TRỊ
!true false
!false true

Ví dụ:

package main

import "fmt"

func main() {
    fmt.Println(true && false)
    fmt.Println(true && false)
    fmt.Println(true || true)
    fmt.Println(true || false)
    fmt.Println(!true)
}

Kết quả:

true
false
true
true
false

Go – Chương trình Hello World

Trong phần này chúng ta sẽ học cách sử dụng Go bằng cách viết một chương trình quen thuộc là chương trình Hello World.

Ví dụ

package main

import "fmt"

// this is a comment 

func main() {
    fmt.Println("Hello World")
}

Chúng ta tạo một file text có tên main.go với nội dung như trên.

Sau đó để dịch và chạy chương trình thì bạn vào Command Prompt (cmd) rồi gõ lệnh go run <tên file>. Bạn có thể phải chỉ ra cả đường dẫn đến tên file nếu thư mục hiện tại trong cmd không trùng với thư mục chứa file code. Chẳng hạn file source bạn để trong thư mục C:/Go/main.go thì thư mục hiện tại trong cmd phải là C:/Go.

C:\Go>go run main.go
Hello World

Nếu bạn làm đúng các bước trên thì sau khi gõ lệnh màn hình console sẽ in ra dòng chữ Hello World. Nếu không phải thì tức là bạn đã thực hiện sai ở bước nào đó, có thể là gõ sai code, trình biên dịch cũng sẽ thông báo lỗi ngay cho bạn. Và cũng giống như bất cứ trình biên dịch khác, chỉ cần trong code của bạn có lỗi thì cả chương trình sẽ không thể chạy được.

Giải thích

Một đoạn code chương trình Go được thực thi từ trên xuống dưới và từ trái sang phải giống như đọc một cuốn sách vậy.

package main

Dòng đầu tiên là package main, đây là câu lệnh khai báo “gói”. Tất cả mọi chương trình Go đều phải được bắt đầu bởi câu lệnh khai báo gói. Tính năng Gói có tác dụng giúp chúng ta tổ chức code và tái sử dụng code dễ dàng hơn, chúng ta sẽ tìm hiểu thêm về gói trong các bài sau.

Khi biên dịch code Go thì có 2 loại là biên dịch thành chương trình chạy trực tiếp (executable) và biên dịch thành thư viện (library). Chương trình chạy trực tiếp là các file khả thi (có đuôi .exe trong Windows) có thể chạy một cách trực tiếp trong terminal (Command Prompt trong Windows). Còn thư viện là tập hợp code được gom lại với nhau và có thể được sử dụng trong các chương trình khác, chúng ta sẽ tìm hiểu về thư viện sau. Hiện tại chỉ bạn chỉ cần biết là cần phải có câu lệnh khai báo package trong code của mình.

Sau dòng khai báo package là một dòng trống, giống như các ngôn ngữ khác, trình biên dịch không quan tâm các các khoảng trống này, chúng ta chỉ dùng chúng để đọc code cho dễ hơn thôi.

import "fmt"

Từ khóa import có nghĩa là chúng ta yêu cầu được sử dụng code từ các gói khác trong chương trình của chúng ta. Ở đây là gói fmt (viết tắt của format), gói này chủ yếu chứa code thực hiện việc định dạng dữ liệu ra/vào.

Khi dùng từ khóa import thì tên gói được đặt trong cặp dấu nháy kép. Những kí tự được bọc trong cặp dấu nháy kép đều được gọi chung là chuỗi (hoặc string trong tiếng Anh), chúng ta sẽ tìm hiểu về string sau.

// this is a comment

Ký tự // cho biết những kí tự đứng sau nó là câu bình luận (comment). Các câu bình luận sẽ không được biên dịch. Trong Go có 2 loại comment, // là loại comment trên một dòng, tất cả các kí tự phía sau // sẽ không được biên dịch, và /* */ là loại comment có thể sử dụng trên nhiều dòng, tất cả các kí tự nằm trong cặp dấu /* */ sẽ không được biên dịch.

func main() {
    fmt.Println("Hello World");
}

Tiếp theo là phần khai báo hàm. Hàm là các thành phần xây dựng nên một chương trình. Hàm nhận dữ liệu vào, xử lý dữ liệu và xuất dữ liệu ra. Tất cả các hàm trong Go đều được định nghĩa bởi từ khóa func, theo sau là tên hàm (ở đây chúng ta định nghĩa hàm có tên main), tiếp theo là cặp dấu (), bên trong cặp dấu này chúng ta có thể khai báo một danh sách các tham số, tiếp theo là kiểu dữ liệu trả về (ở đây chúng ta không khai báo), rồi đến phần thân hàm nằm trong cặp dấu ngoặc nhọn {}, thân hàm chứa các câu lệnh, ở đây chúng ta chỉ có duy nhất một câu lệnh. Chúng ta sẽ tìm hiểu thêm về hàm sau.

Ngoài ra cái tên main là một cái tên đặc biệt, hàm main tự động được hệ điều hành “gọi” đến đầu tiên khi chạy từ file khả thi.

fmt.Println("Hello World");

Bên trong hàm main chúng ta chỉ có một câu lệnh. Câu lệnh này có 3 phần. Câu lệnh này gọi hàm Println() trong gói fmt. Hàm này nhận vào tham số là một string có tên “Hello World”. Hàm Println (viết tắt của print line) thực hiện in chuỗi mà nó nhận được ra màn hình.

Bạn có thể tìm hiểu thêm về hàm Println của gói fmt qua lệnh godoc:

C:\User\PhoCode>godoc fmt Println
use 'godoc cmd/fmt' for documentation on the fmt command

func Println(a ...interface{}) (n int, err error)
    Println formats using the default forrmats for its operands and writes to 
    standard output. Spaces are always added between operands and a newline is appended. 
    It returns the number of bytes written and any write error encountered.

Tài liệu của Go được viết rất kỹ nhưng nếu bạn chưa học lập trình bao giờ thì đọc sẽ hơi thấy khó hiểu, ngoài ra tài liệu chủ yếu tiếng Anh là chính.

Go – Giới thiệu

Go là một ngôn ngữ lập trình được thiết kế dựa trên tư duy lập trình hệ thống. Go được phát triển bởi Robert Griesemer, Rob Pike và Ken Thompson tại Google vào năm 2007. Điểm mạnh của Go là bộ thu gom rác và hỗ trợ lập trình đồng thời (tương tự như đa luồng – multithreading). Go là một ngôn ngữ biên dịch như C/C++, Java, Pascal… Go được giới thiệu vào năm 2009 và được sử dụng hầu hết trong các sản phẩm của Google.

Một số đặc điểm

  • Hỗ trợ khai báo kiểu dữ liệu động
  • Tốc độ biên dịch nhanh
  • Hỗ trợ các tác vụ đồng thời
  • Ngôn ngữ đơn giản, ngắn gọn

Tuy nhiên chính vì muốn ngôn ngữ này trở nên cực kỳ đơn giản mà các nhà phát triển đã loại bỏ một số tính năng (mà mình cho là hữu ích) có trong các ngôn ngữ khác như:

  • Không hỗ trợ thừa kế
  • Không hỗ trợ quá tải toán tử hoặc ghi đè phương thức
  • Không hỗ trợ thao tác trên con trỏ (vì lý do bảo mật)
  • Không hỗ trợ kiểu Generic (giống như template trong C++)

Go có trang chủ tại địa chỉ golang.org.

Cài đặt Go

Bạn download Go tại địa chỉ: https://golang.org/dl/

Hiện tại có 3 phiên bản dành cho 3 dòng hệ điều hành là Windows, Linux và MacOS và bộ mã nguồn. Ứng với mỗi dòng hệ điều hành lại có bản cho 32-bit và 64-bit. Bạn có thể chọn download file ZIP hoặc trình Installer. Bạn download phiên bản phù hợp về rồi cài đặt/giải nén vào nơi mà mình thích.

Có lưu ý là đối với trình Installer thì mặc định Go sẽ được cài vào đường dẫn C:/go và trình Installer sẽ tự động thêm đường dẫn C:/go/bin vào biến môi trường PATH. Nếu bạn chọn cách giải nén bằng file ZIP hoặc dùng Installer mà cài vào đường dẫn khác C:/go thì bạn phải thiết lập biến môi trường này bằng tay, ngoài ra bạn còn phải tạo thêm một biến môi trường khác là GOROOT và cho biến này trỏ tới thư mục mà bạn đã cài đặt Go.

Xem phiên bản Go

Sau khi đã cài đặt Go, bạn có thể mở Command Prompt lên và gõ lệnh go version để xem phiên bản Go đã cài đặt, bạn có thể phải restart lại máy nếu Command Prompt báo không tìm thấy lệnh go.

C:/User/PhoCode>go version
go version go1.7 windows/amd64

Hiện tại phiên bản mà mình sử dụng để viết series này là phiên bản 1.7.

Ngoài ra bạn có thể xem danh sách tham số khác bằng cách gõ lệnh go help.