Dari Python Ke Go
Tulisan ini bukan tentang membandingkan kedua bahasa seperti benchmark performa dan sejenisnya. Tulisan ini hanyalah catatan dari seseorang yang terbiasa menulis Python dan sekarang beralih ke Go.
Typing
Saya bisa dibilang seperti kebanyakan web programmer di Indonesia. Pekerjaan pertama saya adalah PHP – dan otomatis terpapar JavaScript – lalu dilanjutkan dengan Python.
Kedua bahasa tersebut merupakan Dynamic Typing, yang artinya tipe data ditentukan saat runtime, bukan saat deklarasi. Perhatikan kode di bawah:
number = 'string'
character = 123
Deklarasi variabel di atas tidak akan menghasilkan error. Maka, ketika saya pindah ke Go, memang terasa sedikit "kagok" karena setiap variabel perlu dideklarasikan tipe datanya:
var number int
var character string
number = '123'
character = 123
Di Golang, kode di atas tidak akan pernah bisa dijalankan. Awalnya memang terasa kaku, tetapi setelah dipikir ulang, menentukan tipe data—terlepas dari bahasa pemrograman apa pun yang digunakan—pasti dilakukan, setidaknya saat menentukan struktur tabel di database.
Error
Saat menulis kode yang kemungkinan menghasilkan galat (error), dalam Python umumnya kode akan dibungkus ke dalam try-except:
try:
result = 10 / 0
except ZeroDivisionError:
print("You can't divide by zero!")
atau secara teori strukturnya akan seperti berikut:
try:
code
except TypeError:
handle_error()
finally:
print("This always runs")
Di Go, error diperlakukan "sebatas value"
kalian bisa baca alasan lebih teknisnya di sini https://go.dev/blog/errors-are-values
Jadi, jika menjalankan sebuah fungsi yang berpotensi menghasilkan error, umumnya kodenya akan seperti berikut:
success, err := SomeFunction()
Struktur kode di atas yang akhirnya menghasilkan "3 line of code" paling populer dalam diskursus Go:
if err != nil {
// do something
}
bahkan saat ini sedang ada diskusi mengenai error di go https://github.com/golang/go/discussions/71460
Bagi saya, permasalahannya mungkin karena ada banyak pengulangan dalam pengecekan error:
s, err := first()
if err != nil {}
x, err := second()
if err != nil{}
...
Tapi jika permasalahannya hanya visual, penggunaan IDE bisa membantu meminimalisir hal tersebut.
Bagi saya yang sebelumnya banyak menggunakan try-except dan sekarang beralih ke if err != nil {}
, perubahan ini mengubah paradigma berpikir. Dalam Python, saya terkadang berpikir:
"Jika error terjadi—terlepas bagian mana pun yang error—saya harus melakukan apa?"
Sedangkan di Go, saya harus berpikir:
"Bagian ini bisa error, saya harus apa? Bagian sana juga kemungkinan error, harus diapakan?"
Sejauh ini, saya masih asik-asik saja menulis if err != nil {}
. Entah jika suatu saat saya mencoba bahasa pemrograman lain dan ternyata pengelolaan error-nya bisa lebih menyenangkan.
Function/Method
Terlalu lama menggunakan Django, biasanya terbentuk dua opsi saat menulis sebuah fitur: CBV vs FBV. Di Go, saya menemukan "kesederhanaan" — semua ditulis adalah function atau method.
Jika di Python – Django spesifiknya – lumrah menulis antara dua hal ini:
# fbv
def view_method(request):
do_something()
#cbv
class ViewApi(Generic):
do_something()
Saat di Go,cukup:
func someFunc() {
do_something()
}
Atau menggunakan method
type User struct {
firstName, lastName string
}
func (u *User) fullName() string {
return u.firstName + " " + u.lastName
}
func main() {
user := User{firstName: "John", lastName: "Doe"}
fmt.Println(user.fullName())
}
type Service struct {
user lib.UserInterface
storage lib.StorageInterface
}
func NewService(storage lib.StorageInterface, user lib.UserInterface) *Service {
return &Service{
storage: storage,
user: user,
}
}
func (s *Service) CreateBlog(ctx context.Context, in *lib.Input) *lib.Output {
resp := lib.Output{}
// validation
if in.title == "" {
// do something
}
// call other service
user := s.user.Get(ctx, in)
if user.Success == false {
// do something
}
storage, err := s.storage.Begin(ctx)
if err != nil {
log.Warn(in.Trace).Msg(" failed begin tx")
return resp
}
defer storage.Rollback(ctx)
// rest of code
return &resp
}
Table test
Di Go, sayapun menemukan kenyamanan dalam membuat test. Membuat test di Go bisa sangat sederhana, tapi cukup lengkap jika kita memiliki beragam skenario.
func TestGetUserWithScenario(t *testing.T) {
t.Parallel()
con := test.DbTestPool(t)
// create the storage
userStorage := NewStorage(con.Pool)
//prepare the fixtures
userFixtures := map[int]*data.User{
1: {
Fullname: "User Test",
Username: "testuser",
Email: "[email protected]",
Password: "hashedpassword1",
},
2: {
Fullname: "User Test 2",
Username: "testuser22",
Email: "[email protected]",
Password: "hashedpassword1",
},
3: {
Fullname: "User Test 3",
Username: "testuser33",
Email: "[email protected]",
Password: "hashedpassword1",
},
}
//insert the fixtures
storage, err := userStorage.BeginTxWriter(con.Context)
assert.Nil(t, err)
defer storage.Rollback(con.Context)
// Create test user
for key, v := range userFixtures {
id, err := userStorage.InsertUser(con.Context,
v.Fullname, v.Username, v.Email, v.Password,
)
if err != nil {
t.Fatal("Error creating user fixture:", err)
}
userFixtures[key].Id = id
}
err = storage.Commit(con.Context)
if err != nil {
t.Fatal(err)
}
type input struct {
id int
}
type expected struct {
success bool
data *data.User
}
type testCase struct {
name string
input input
expected expected
}
scenarios := []testCase{
{
name: "successfully get existing user index 1",
input: input{
id: userFixtures[1].Id, // Ensure valid key exists
},
expected: expected{
success: true,
data: userFixtures[1], // Using the key for validation
},
},
{
name: "successfully get existing user index 2",
input: input{
id: userFixtures[2].Id, // Valid key for the second user
},
expected: expected{
success: true,
data: userFixtures[2], // Using the key for validation
},
},
{
name: "returns error when user not found",
input: input{
id: 999, // Non-existing ID
},
expected: expected{
success: false,
data: nil,
},
},
}
storage, err = userStorage.BeginTxReader(con.Context)
assert.Nil(t, err)
defer storage.Rollback(con.Context)
for _, sc := range scenarios {
t.Run(sc.name, func(t *testing.T) {
found, errType, err := userStorage.GetUser(con.Context, sc.input.id)
if sc.expected.success == false {
assert.NotNil(t, err)
assert.Equal(t, database.ErrNotFound, errType)
assert.Nil(t, found)
return
}
assert.Nil(t, err)
sc.expected.data.CreatedAt = found.CreatedAt
sc.expected.data.UpdatedAt = found.UpdatedAt
assert.Equal(t, database.ErrUnset, errType)
assert.Equal(t, sc.expected.data, found)
})
}
}
Untuk mendapatkan hal seperti itu biasanya di python saya menggunakan pytest-paramterize
testdata = [
(datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
(datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
]
@pytest.mark.parametrize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected):
diff = a - b
assert diff == expected
Kesimpulan
Sejauh ini bisa dibilang masih dalam tahap "bulan madu" dengan Go, mirip saat saya pertama kali pake Python setelah lama di PHP, tapi sejauh ini sih masih sangat menyenangkan menulis kode Go, entah beberapa saat lagi atau mungkin akan berbeda setelah mencoba bahasa lainnya.